<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Srikar Phani Kumar Marti</title>
    <description>The latest articles on DEV Community by Srikar Phani Kumar Marti (@mspk97).</description>
    <link>https://dev.to/mspk97</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2519231%2F04ef8b8c-91fd-4ba1-a9b7-1e98f84baf6a.png</url>
      <title>DEV Community: Srikar Phani Kumar Marti</title>
      <link>https://dev.to/mspk97</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mspk97"/>
    <language>en</language>
    <item>
      <title>Design Systems and Accessibility: Mechanisms to Enforce Consistent Focus Management</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Mon, 05 Oct 2026 11:01:19 +0000</pubDate>
      <link>https://dev.to/mspk97/design-systems-and-accessibility-mechanisms-to-enforce-consistent-focus-management-3930</link>
      <guid>https://dev.to/mspk97/design-systems-and-accessibility-mechanisms-to-enforce-consistent-focus-management-3930</guid>
      <description>&lt;p&gt;Ever been puzzled when tabbing through a complex UI built from a design system, only to have the focus jump in unexpected ways or disappear entirely? Maybe a modal’s close button isn’t reachable by keyboard, or focus lands on something visually hidden. It’s a tiny annoyance for some, but a showstopper for keyboard users and screen reader folks.&lt;/p&gt;

&lt;p&gt;I’ve faced this exact headache while working on a large React design system. The components promised accessibility out of the box, but subtle app-level overrides or custom wrappers sometimes broke the focus flow. Tracking down where the focus management logic lived, and why it wasn’t working, led me down a rabbit hole of internal mechanisms that design systems use to enforce consistent, accessible focus.&lt;/p&gt;

&lt;p&gt;Let me share what I learned about how design systems coordinate focus, how they integrate with ARIA, and how you can debug when your app’s overrides clash with those defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  Focus management is more than tabindex
&lt;/h2&gt;

&lt;p&gt;Setting &lt;code&gt;tabindex&lt;/code&gt; is the classic way to control keyboard focus order. But design systems go way beyond sprinkling tabindex attributes.&lt;/p&gt;

&lt;p&gt;For example, take a dropdown component. When you open it, focus should move into the dropdown’s first interactive item. When you close it, focus needs to return to the button that triggered it. And keyboard navigation inside the dropdown needs to cycle logically.&lt;/p&gt;

&lt;p&gt;Design systems bake these rules into their components using JavaScript focus management. They listen for keyboard events, programmatically call &lt;code&gt;.focus()&lt;/code&gt; on appropriate elements, and maintain internal state about what should be focused next.&lt;/p&gt;

&lt;p&gt;This means a lot of logic lives inside the system’s components, not just in markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How design systems enforce consistent focus
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Centralized focus utilities
&lt;/h2&gt;

&lt;p&gt;Many systems include shared focus utilities: helper functions or hooks that set focus, trap focus within dialogs, or restore focus after navigation. These utilities handle quirks across browsers and assistive tech. &lt;/p&gt;

&lt;p&gt;For example, a "focus trap" utility listens for tab key presses and ensures focus cycles only within a modal’s interactive elements, preventing keyboard users from tabbing to elements behind the modal.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Focus context and state
&lt;/h2&gt;

&lt;p&gt;Some systems create a focus context that tracks the current focus target and allows nested components to coordinate. For example, a tab panel component and its tabs share focus state so that activating a tab moves focus correctly and updates the panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ARIA integration
&lt;/h2&gt;

&lt;p&gt;ARIA attributes are part of the story but not the whole story. Design systems use ARIA roles like &lt;code&gt;role="dialog"&lt;/code&gt;, &lt;code&gt;aria-modal="true"&lt;/code&gt;, and &lt;code&gt;aria-labelledby&lt;/code&gt; to help screen readers understand component purpose and relationships.&lt;/p&gt;

&lt;p&gt;But focus management is still done with JavaScript. For instance, an accessible modal will set &lt;code&gt;aria-hidden="true"&lt;/code&gt; on background content and shift focus to the modal container when opened.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keyboard event handling
&lt;/h2&gt;

&lt;p&gt;Design systems intercept keyboard events like Tab, Shift+Tab, Escape, and arrow keys to customize focus movement and component behavior.&lt;/p&gt;

&lt;p&gt;For example, arrow keys might move focus between menu items, Escape closes a modal and returns focus, and Tab cycles focus inside a focus trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  When your app overrides break focus management
&lt;/h2&gt;

&lt;p&gt;I ran into bugs when app-level styles or wrappers overrode the design system’s components. Here are common pitfalls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom wrappers missing focus forwarding:&lt;/strong&gt; If you wrap a focusable component but don’t forward refs properly, programmatic &lt;code&gt;.focus()&lt;/code&gt; calls inside the system fail silently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CSS hiding focus indicators:&lt;/strong&gt; Overriding focus styles with &lt;code&gt;outline: none&lt;/code&gt; or other rules can make focus invisible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conflicting event handlers:&lt;/strong&gt; Adding keyboard handlers that prevent default behavior or stop propagation can break the system’s internal focus logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic rendering mismatches:&lt;/strong&gt; Rendering components conditionally without coordinating focus restoration leads to lost focus or unexpected jumps.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debugging focus issues in design systems
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Use browser devtools’ accessibility inspectors
&lt;/h2&gt;

&lt;p&gt;Browsers like Chrome and Firefox have accessibility panes showing the accessibility tree and focusable elements. Use these to verify which elements are focusable and whether ARIA attributes are set correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual focus inspection
&lt;/h2&gt;

&lt;p&gt;Try tabbing through your app step by step. Note where focus lands, where it disappears, or where it loops unexpectedly.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;document.activeElement&lt;/code&gt; in the console to see the current focused element at any point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check event listeners
&lt;/h2&gt;

&lt;p&gt;Inspect the event listeners on elements to see if keyboard events are handled or blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify ref forwarding
&lt;/h2&gt;

&lt;p&gt;If your components use React, ensure ref forwarding is correctly set up so the design system can call &lt;code&gt;.focus()&lt;/code&gt; on the right DOM node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test with screen readers
&lt;/h2&gt;

&lt;p&gt;Keyboard focus and visual focus indicators are only part of accessibility. Test with screen readers like NVDA or VoiceOver to make sure focus changes align with announcements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Focus trap in a modal
&lt;/h2&gt;

&lt;p&gt;Here’s a simplified example of how a focus trap works inside a modal component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modalRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focusableElements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;modalRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleKeyDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shiftKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;firstElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nx"&gt;lastElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lastElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nx"&gt;firstElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Escape&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;modalRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleKeyDown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;firstElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;modalRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleKeyDown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt; &lt;span class="na"&gt;aria-modal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;modalRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet traps focus inside the modal while it’s open and closes on Escape. Design systems build on this idea but handle edge cases and browser quirks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Focus management inside design systems is a mix of ARIA, JavaScript, event handling, and CSS working together. It’s not enough to add &lt;code&gt;tabindex&lt;/code&gt; and call it done.&lt;/p&gt;

&lt;p&gt;If you build or maintain a design system, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Providing standardized focus utilities&lt;/li&gt;
&lt;li&gt;Documenting expected focus behavior for each component&lt;/li&gt;
&lt;li&gt;Testing with keyboard and screen readers&lt;/li&gt;
&lt;li&gt;Encouraging app teams to respect focus management and avoid harmful overrides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re integrating a design system and hit focus bugs, dig into the event handling, ref forwarding, and CSS focus styles. Use browser devtools and assistive tech to observe what’s happening.&lt;/p&gt;

&lt;p&gt;These mechanisms might feel like plumbing, but getting focus right makes your UI genuinely usable for everyone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/design-systems-and-accessibility-mechanisms-to-enforce-consistent-focus-management" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>designsystems</category>
      <category>frontend</category>
      <category>focusmanagement</category>
    </item>
    <item>
      <title>Understanding React’s useTransition: Mechanisms and Performance Implications</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:18:40 +0000</pubDate>
      <link>https://dev.to/mspk97/understanding-reacts-usetransition-mechanisms-and-performance-implications-2efl</link>
      <guid>https://dev.to/mspk97/understanding-reacts-usetransition-mechanisms-and-performance-implications-2efl</guid>
      <description>&lt;p&gt;Ever had a React app where an update causes a janky UI or a spinner that pops up out of nowhere ,  but only sometimes? I ran into this recently when trying to make a search input feel smooth while fetching results. I used &lt;code&gt;useTransition&lt;/code&gt; to defer the heavy update, but I didn’t fully grasp what React was doing behind the scenes. &lt;/p&gt;

&lt;p&gt;Turns out, &lt;code&gt;useTransition&lt;/code&gt; is not just a fancy hook to slap on some state. It’s a clever way React’s concurrent rendering lets you mark updates as "non-urgent." That impacts scheduling, priority, and how your UI feels to users. &lt;/p&gt;

&lt;p&gt;Let me walk you through what really happens under the hood with &lt;code&gt;useTransition&lt;/code&gt;, how it interacts with React’s scheduler, and when it actually helps your app’s performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The developer moment: Why did my spinner randomly appear?
&lt;/h2&gt;

&lt;p&gt;I had a component with a search box. Typing updated a local state immediately (the input’s value), but then I also kicked off a data fetch and updated the results state. The results update triggered a big list render, which sometimes made typing laggy.&lt;/p&gt;

&lt;p&gt;I wrapped the results state update in a &lt;code&gt;startTransition&lt;/code&gt; callback from &lt;code&gt;useTransition&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTransition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// urgent update, keep input responsive&lt;/span&gt;
  &lt;span class="nf"&gt;startTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;fetchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// deferred update&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now React shows a spinner when &lt;code&gt;isPending&lt;/code&gt; is true, which means the transition is in flight. But the spinner sometimes flashes for just a split second or not at all. Why?&lt;/p&gt;

&lt;p&gt;The answer lies in how React prioritizes these updates and schedules rendering work. &lt;/p&gt;

&lt;h2&gt;
  
  
  useTransition under the hood: marking updates as low priority
&lt;/h2&gt;

&lt;p&gt;React’s concurrent mode lets your app interrupt rendering work to keep the UI responsive. Priorities matter. User input is high priority. Visual updates that could wait? Lower priority.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useTransition&lt;/code&gt; leverages this by tagging state updates inside &lt;code&gt;startTransition&lt;/code&gt; as "transitions": low priority work.&lt;/p&gt;

&lt;p&gt;When you call &lt;code&gt;startTransition(() =&amp;gt; setState(newValue))&lt;/code&gt;, React:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Marks that update as &lt;em&gt;non-urgent&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Schedules it with a lower priority than input or animation updates.&lt;/li&gt;
&lt;li&gt;Allows React to keep rendering high priority work first (like updating the input value).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means your input feels snappy even if the results list takes time to render or fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens in React’s scheduler?
&lt;/h2&gt;

&lt;p&gt;React’s scheduler manages a queue of updates with priorities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High priority: user input, clicks, focus&lt;/li&gt;
&lt;li&gt;Transition priority: updates inside &lt;code&gt;startTransition&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Normal priority: other updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine React like a cook in a busy kitchen. Urgent orders (user input) get served immediately. Less urgent ones (transitions) wait for a free moment.&lt;/p&gt;

&lt;p&gt;When a transition update is scheduled, React tries to render it in chunks, yielding control back to the browser if there’s more urgent work.&lt;/p&gt;

&lt;p&gt;If the user keeps typing fast, React can pause rendering the transition and show the latest input first.&lt;/p&gt;

&lt;p&gt;This is why your spinner may flash briefly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the transition finishes very fast, React commits it immediately, so &lt;code&gt;isPending&lt;/code&gt; toggles quickly.&lt;/li&gt;
&lt;li&gt;If the user interrupts by typing more, React may drop the previous transition and start a new one, causing flickers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;isPending&lt;/code&gt; flag: what you’re really tracking
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useTransition&lt;/code&gt; gives you &lt;code&gt;isPending&lt;/code&gt;, a boolean that indicates if there’s any transition update still "in progress." &lt;/p&gt;

&lt;p&gt;But "in progress" here means "React hasn’t committed the transition update to the DOM yet."&lt;/p&gt;

&lt;p&gt;It doesn’t mean your fetch is still pending (you have to track that yourself). It means React’s rendering of the update is ongoing or waiting.&lt;/p&gt;

&lt;p&gt;Because React batches and may interrupt work, &lt;code&gt;isPending&lt;/code&gt; can toggle quickly, making spinners flash unexpectedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use useTransition ,  and when not to
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useTransition&lt;/code&gt; shines when you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep input or UI responsiveness snappy by deferring heavy updates&lt;/li&gt;
&lt;li&gt;Show some feedback (like a spinner) while transition updates are rendering&lt;/li&gt;
&lt;li&gt;Avoid blocking urgent updates with less urgent ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it’s not a silver bullet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your deferred update is tiny or fast, the spinner can feel like flicker. You might prefer to skip it.&lt;/li&gt;
&lt;li&gt;If you don’t handle loading states well, users might get confused.&lt;/li&gt;
&lt;li&gt;If you’re not in concurrent mode or React 18+, &lt;code&gt;useTransition&lt;/code&gt; won’t do much.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A concrete example: search input with and without useTransition
&lt;/h2&gt;

&lt;p&gt;Imagine a big list that takes 300ms to render.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;useTransition&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User types a letter&lt;/li&gt;
&lt;li&gt;React updates query and results immediately&lt;/li&gt;
&lt;li&gt;UI blocks for 300ms, input lags or freezes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;code&gt;useTransition&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User types a letter&lt;/li&gt;
&lt;li&gt;React updates query immediately (urgent)&lt;/li&gt;
&lt;li&gt;React schedules results update as a transition&lt;/li&gt;
&lt;li&gt;Input stays responsive&lt;/li&gt;
&lt;li&gt;Spinner shows while results render&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if the user types multiple letters quickly, React might skip rendering intermediate results, showing only the latest. The spinner might flash briefly or never appear if rendering is too fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging tip: React DevTools Profiler and &lt;code&gt;isPending&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;React DevTools Profiler can show you when transitions start and end. You’ll see how React schedules and prioritizes updates.&lt;/p&gt;

&lt;p&gt;When debugging flickering spinners or janky UI, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are you using &lt;code&gt;startTransition&lt;/code&gt; correctly?&lt;/li&gt;
&lt;li&gt;Is your deferred update actually expensive?&lt;/li&gt;
&lt;li&gt;How often does &lt;code&gt;isPending&lt;/code&gt; toggle?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also throttle your CPU or network to simulate slow rendering and fetches, making the transition more visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens if you nest transitions or mix priorities?
&lt;/h2&gt;

&lt;p&gt;React lets you nest transitions, but inside a transition, a state update is always low priority unless you explicitly mark it urgent.&lt;/p&gt;

&lt;p&gt;Mixing urgent and transition updates can lead to surprising UI behaviors if not managed carefully.&lt;/p&gt;

&lt;p&gt;For example, if you update input state inside a transition, input responsiveness suffers. Always update urgent UI state outside &lt;code&gt;startTransition&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useTransition&lt;/code&gt; is a subtle but powerful tool to tell React, "This update can wait a bit." It works by marking state updates as low priority, letting React’s concurrent scheduler keep your UI responsive.&lt;/p&gt;

&lt;p&gt;Understanding the scheduler, priorities, and what &lt;code&gt;isPending&lt;/code&gt; really means helps you use &lt;code&gt;useTransition&lt;/code&gt; effectively and avoid flickery spinners or janky typing.&lt;/p&gt;

&lt;p&gt;Next time your React UI feels sluggish or your loading spinners flash unpredictably, remember what’s happening under the hood with transitions ,  it’s a conversation between your code and React’s scheduler to keep things smooth.&lt;/p&gt;

&lt;p&gt;Give it a try in your app, measure with React DevTools, and tweak your priorities to find the sweet spot between responsiveness and smooth updates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/understanding-react-s-usetransition-mechanisms-and-performance-implications" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>usetransition</category>
      <category>concurrentmode</category>
      <category>performance</category>
    </item>
    <item>
      <title>Design Systems and Focus Management: Mechanisms to Avoid Focus Loss and Trap Bugs</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:48:04 +0000</pubDate>
      <link>https://dev.to/mspk97/design-systems-and-focus-management-mechanisms-to-avoid-focus-loss-and-trap-bugs-3b8e</link>
      <guid>https://dev.to/mspk97/design-systems-and-focus-management-mechanisms-to-avoid-focus-loss-and-trap-bugs-3b8e</guid>
      <description>&lt;p&gt;Ever had that moment where you open a modal or dropdown, start tabbing through, and suddenly the keyboard focus vanishes into thin air? Or worse, you get stuck inside a widget with no way out without a mouse? &lt;/p&gt;

&lt;p&gt;I’ve been there, frustrated and scratching my head, especially when these bugs crop up in polished design systems you’d expect to handle this smoothly.&lt;/p&gt;

&lt;p&gt;Focus management is one of those invisible, tricky parts of UI that make or break accessibility. Behind the scenes, design systems invest surprisingly complex logic to make sure keyboard users never lose their spot or get trapped.&lt;/p&gt;

&lt;p&gt;Let me walk you through what I learned digging into how popular design systems manage focus internally, the kinds of focus trap bugs that still sneak in, and how you can debug these issues in your own complex components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Focus Management Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;On the surface, managing focus sounds simple: when a user tabs, move focus to the next logical element. When a modal opens, focus the first input. When it closes, return focus back.&lt;/p&gt;

&lt;p&gt;But under the hood, several tricky things happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus loss:&lt;/strong&gt; When you remove or hide elements, the browser may move focus unexpectedly or nowhere at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus traps:&lt;/strong&gt; Modals and popovers often want to trap focus inside, so keyboard users can’t tab out accidentally. But implementing this trap correctly is subtle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested components:&lt;/strong&gt; Complex component trees with nested modals, popovers, dropdowns, and tooltips can confuse the browser’s native focus order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic DOM changes:&lt;/strong&gt; Reactivity, animations, and mounting/unmounting components can cause focus to jump or disappear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? Bugs like keyboard users getting stuck inside a modal, focus jumping unpredictably, or focus disappearing completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Peek Under the Hood: How Design Systems Manage Focus
&lt;/h2&gt;

&lt;p&gt;I looked into a few popular design systems ,  think Material UI, Chakra UI, and Reach UI ,  and here’s how they tackle the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Focus Scope and Focus Trap
&lt;/h2&gt;

&lt;p&gt;Most systems create a "focus trap" around modals and popovers. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They listen to keyboard events, especially Tab and Shift+Tab.&lt;/li&gt;
&lt;li&gt;When the user tabs forward from the last focusable element inside the trap, they move focus back to the first.&lt;/li&gt;
&lt;li&gt;When the user tabs backward from the first element, they move focus to the last.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This cyclical focus prevents the keyboard from escaping the modal unintentionally.&lt;/p&gt;

&lt;p&gt;Underneath, they either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use invisible sentinel elements (focus sentinels) before and after the trap to catch tab events.&lt;/li&gt;
&lt;li&gt;Or listen to &lt;code&gt;keydown&lt;/code&gt; events and manually call &lt;code&gt;event.preventDefault()&lt;/code&gt; and &lt;code&gt;focus()&lt;/code&gt; to redirect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a simplified example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;trapFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focusableElements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFocusableElements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focusedIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shiftKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;focusedIndex&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shiftKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;focusedIndex&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;focusableElements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Restoring Focus on Close
&lt;/h2&gt;

&lt;p&gt;Good design systems save the currently focused element before opening a modal or popover and restore focus to it when the overlay closes.&lt;/p&gt;

&lt;p&gt;This seems straightforward but can get tricky when the original element unmounts or the DOM changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Managing Focus on Mount
&lt;/h2&gt;

&lt;p&gt;When a modal opens, the ideal is to focus the first interactive element inside. But what if there’s no focusable element? Or the element appears after an animation?&lt;/p&gt;

&lt;p&gt;Some systems wait until the content is fully rendered or use a &lt;code&gt;setTimeout&lt;/code&gt; to delay focusing.&lt;/p&gt;

&lt;p&gt;Others allow you to specify which element should receive focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Handling Nested Focus Traps
&lt;/h2&gt;

&lt;p&gt;What if you have a modal inside a modal? Or a dropdown inside a popover?&lt;/p&gt;

&lt;p&gt;Design systems keep track of nested traps using stacks. The most recently opened trap is active, while others are paused or disabled.&lt;/p&gt;

&lt;p&gt;This ensures keyboard navigation respects the innermost overlay first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Focus Trap Bugs and What Causes Them
&lt;/h2&gt;

&lt;p&gt;Despite best efforts, focus bugs still happen, especially in custom or complex UIs.&lt;/p&gt;

&lt;p&gt;Here are some patterns I’ve seen:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Phantom Focus Bug
&lt;/h2&gt;

&lt;p&gt;Focus disappears completely. Keyboard users tab, but nothing highlights.&lt;/p&gt;

&lt;p&gt;Usually caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The focused element being removed or hidden without focus moving elsewhere.&lt;/li&gt;
&lt;li&gt;Focus being moved to a non-focusable container.&lt;/li&gt;
&lt;li&gt;Timing issues where focus is set before the element is in the DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Focus Loop Break
&lt;/h2&gt;

&lt;p&gt;Tabbing forward or backward escapes the trap unexpectedly.&lt;/p&gt;

&lt;p&gt;Often caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing or broken sentinel elements.&lt;/li&gt;
&lt;li&gt;Multiple event listeners fighting over focus.&lt;/li&gt;
&lt;li&gt;Nested traps not properly prioritized.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Focus Trap Lock
&lt;/h2&gt;

&lt;p&gt;Focus gets stuck inside a component with no way out.&lt;/p&gt;

&lt;p&gt;Caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failing to restore focus to the element that opened the trap.&lt;/li&gt;
&lt;li&gt;Keyboard handlers swallowing Tab or Shift+Tab but not redirecting focus.&lt;/li&gt;
&lt;li&gt;Overly aggressive focus locking when multiple overlays are open.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debugging Focus Issues in Complex Trees
&lt;/h2&gt;

&lt;p&gt;When your UI has nested components, portals, and lots of dynamic elements, debugging focus gets tricky.&lt;/p&gt;

&lt;p&gt;Here’s a practical approach:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Visualize Focus
&lt;/h2&gt;

&lt;p&gt;Use Chrome DevTools or Firefox to inspect the focused element:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In DevTools console, &lt;code&gt;document.activeElement&lt;/code&gt; tells you where focus currently is.&lt;/li&gt;
&lt;li&gt;Add a global CSS style like &lt;code&gt;*:focus { outline: 2px solid hotpink !important; }&lt;/code&gt; to clearly see focus.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Log Focus Events
&lt;/h2&gt;

&lt;p&gt;Listen for &lt;code&gt;focusin&lt;/code&gt; and &lt;code&gt;focusout&lt;/code&gt; on the document to track focus movement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focusin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus in:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focusout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus out:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps catch unexpected focus jumps or losses.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check Focusable Elements
&lt;/h2&gt;

&lt;p&gt;Run a quick helper to list focusable elements inside your trap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getFocusableElements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure you actually have focusable elements where you expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Inspect Event Listeners
&lt;/h2&gt;

&lt;p&gt;Use DevTools to see which event listeners are attached to trap elements. Conflicting listeners can break focus management.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test Keyboard Navigation Manually
&lt;/h2&gt;

&lt;p&gt;Slowly tab through your UI and watch focus move. Try shift+tab. Notice where focus gets stuck or lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Look for Portals and DOM Moves
&lt;/h2&gt;

&lt;p&gt;Elements rendered via portals or outside the normal DOM flow can confuse focus. Make sure your focus trap accounts for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real-World Example: Debugging a Focus Trap Bug in a Modal
&lt;/h2&gt;

&lt;p&gt;I recently helped debug a modal that sometimes lost focus after opening.&lt;/p&gt;

&lt;p&gt;The symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open modal&lt;/li&gt;
&lt;li&gt;Keyboard focus briefly appeared on the first button&lt;/li&gt;
&lt;li&gt;Focus disappeared and tabbing did nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After some digging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The modal content was rendered asynchronously with a slight delay.&lt;/li&gt;
&lt;li&gt;The focus trap attempted to focus the button immediately on open.&lt;/li&gt;
&lt;li&gt;Because the button wasn’t in the DOM yet, focus landed nowhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delay the initial focus call until after the modal content mounted.&lt;/li&gt;
&lt;li&gt;Use a &lt;code&gt;useEffect&lt;/code&gt; hook with dependencies on modal open state.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;firstButtonRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tiny delay made all the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Focus management is a delicate balancing act hidden under the hood of every accessible UI component. Design systems invest a lot of subtle logic to keep keyboard users on track, avoid traps, and restore focus correctly.&lt;/p&gt;

&lt;p&gt;If you build custom components or integrate third-party design systems, understanding these mechanisms helps you avoid nasty focus bugs and debug them faster when they do appear.&lt;/p&gt;

&lt;p&gt;Keep an eye on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focus traps and how Tab/Shift+Tab keys behave&lt;/li&gt;
&lt;li&gt;Focus restoration after overlays close&lt;/li&gt;
&lt;li&gt;Timing issues with dynamic rendering&lt;/li&gt;
&lt;li&gt;Nested traps and portals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And don’t forget to test keyboard navigation as early and often as you can.&lt;/p&gt;

&lt;p&gt;Your keyboard users will thank you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/design-systems-and-focus-management-mechanisms-to-avoid-focus-loss-and-trap-bugs" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>focusmanagement</category>
      <category>designsystems</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How Browsers Handle Keyboard Focus Navigation with tabindex -1 and aria-activedescendant</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Wed, 23 Sep 2026 10:46:08 +0000</pubDate>
      <link>https://dev.to/mspk97/how-browsers-handle-keyboard-focus-navigation-with-tabindex-1-and-aria-activedescendant-3ai2</link>
      <guid>https://dev.to/mspk97/how-browsers-handle-keyboard-focus-navigation-with-tabindex-1-and-aria-activedescendant-3ai2</guid>
      <description>&lt;p&gt;Ever been stuck debugging a custom dropdown or a combo box that ignores keyboard focus in weird ways? Maybe you made the list items &lt;code&gt;tabindex="-1"&lt;/code&gt; so they’re not in the tab order, and relied on &lt;code&gt;aria-activedescendant&lt;/code&gt; to point to the selected item. But then, pressing arrow keys doesn’t update the screen reader’s focus the way you expect. Or worse, the browser’s native focus ring seems to vanish.&lt;/p&gt;

&lt;p&gt;I hit exactly this while building a complex widget with a roving tabindex pattern. It looked perfect visually, but keyboard users and screen reader users got lost. Turns out, understanding how browsers handle &lt;code&gt;tabindex="-1"&lt;/code&gt; and &lt;code&gt;aria-activedescendant&lt;/code&gt; isn’t just about reading specs ,  it’s about grasping what the browser &lt;em&gt;really&lt;/em&gt; does with keyboard focus under the hood.&lt;/p&gt;

&lt;p&gt;Let me share what I learned about these two key pieces, how browsers manage focus, and how you can spot and fix common pitfalls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The curious case of tabindex="-1"
&lt;/h2&gt;

&lt;p&gt;You probably know: &lt;code&gt;tabindex="0"&lt;/code&gt; makes an element keyboard focusable in the natural tab order. &lt;code&gt;tabindex="-1"&lt;/code&gt; makes an element focusable &lt;em&gt;programmatically&lt;/em&gt;, but skips it in the tab sequence.&lt;/p&gt;

&lt;p&gt;That sounds straightforward. But here’s the catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can call &lt;code&gt;.focus()&lt;/code&gt; on a &lt;code&gt;tabindex="-1"&lt;/code&gt; element, and it accepts keyboard focus.&lt;/li&gt;
&lt;li&gt;But pressing Tab will &lt;em&gt;never&lt;/em&gt; land on it.&lt;/li&gt;
&lt;li&gt;Some browsers show the focus ring only for keyboard-initiated focus (Tab key), so focusing programmatically might hide the ring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why use &lt;code&gt;tabindex="-1"&lt;/code&gt; at all? Because sometimes you want to move focus behind the scenes ,  like when a custom widget manages selection and keyboard navigation internally.&lt;/p&gt;

&lt;p&gt;Example: A combo box where the input is focused, but the active item in the dropdown is &lt;em&gt;not&lt;/em&gt; focused in the DOM, only referenced via &lt;code&gt;aria-activedescendant&lt;/code&gt;. The list items have &lt;code&gt;tabindex="-1"&lt;/code&gt; so they’re not tabbable, but can be focused by scripts if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about aria-activedescendant?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aria-activedescendant&lt;/code&gt; is a little-known but powerful ARIA attribute. It tells assistive technologies: "Hey, the element with this id inside me is the active item." The focus stays on the container, but the screen reader &lt;em&gt;virtually&lt;/em&gt; moves focus to the referenced element.&lt;/p&gt;

&lt;p&gt;This is crucial for widgets like listboxes or comboboxes where focus should remain on the input or container, but the active selection changes.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;aria-activedescendant&lt;/code&gt; only works if the container element itself is keyboard focusable (usually &lt;code&gt;tabindex="0"&lt;/code&gt; or naturally focusable). If the container isn’t focused, the screen reader has no anchor to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  How browsers actually manage keyboard focus here
&lt;/h2&gt;

&lt;p&gt;Here’s the key: browsers track a single &lt;em&gt;focused element&lt;/em&gt; in the DOM ,  the one that receives keyboard events, shows the native focus ring, and is the anchor for assistive tech.&lt;/p&gt;

&lt;p&gt;When you set &lt;code&gt;aria-activedescendant&lt;/code&gt; on that focused element, screen readers use it to redirect their virtual focus indication to the referenced item.&lt;/p&gt;

&lt;p&gt;But the referenced item itself is &lt;em&gt;not&lt;/em&gt; focused. It’s just pointed to.&lt;/p&gt;

&lt;p&gt;If you try to focus an item with &lt;code&gt;tabindex="-1"&lt;/code&gt; programmatically (like calling &lt;code&gt;.focus()&lt;/code&gt;), that item becomes the browser focus target. But if you want to keep the input focused and just update the virtual active descendant, don’t call &lt;code&gt;.focus()&lt;/code&gt; on the list item.&lt;/p&gt;

&lt;p&gt;This distinction matters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The native focus ring appears on the &lt;em&gt;focused element&lt;/em&gt; (the container or input), not the active descendant.&lt;/li&gt;
&lt;li&gt;Keyboard events go to the focused element.&lt;/li&gt;
&lt;li&gt;Screen readers announce the active descendant as the "current item" inside the container.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common traps and debugging tips
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Trap 1: Forgetting to focus the container
&lt;/h2&gt;

&lt;p&gt;If your container element (the one with &lt;code&gt;aria-activedescendant&lt;/code&gt;) isn’t focused, screen readers won’t follow the active descendant. Your list items might update visually, but the a11y focus won’t move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Make sure the container is focusable (&lt;code&gt;tabindex="0"&lt;/code&gt; or native) and programmatically focus it on widget open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: Setting tabindex="-1" on active items and calling &lt;code&gt;.focus()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you call &lt;code&gt;.focus()&lt;/code&gt; on a &lt;code&gt;tabindex="-1"&lt;/code&gt; list item, you move actual DOM focus there, which breaks keyboard input on the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Instead of moving DOM focus, update the container’s &lt;code&gt;aria-activedescendant&lt;/code&gt; to point to the active item. Keep focus on the container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: Expecting native focus ring on active descendant
&lt;/h2&gt;

&lt;p&gt;Browsers don’t show the native focus ring on the active descendant because it’s not actually focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use CSS styles on the active item to highlight it visually. Use &lt;code&gt;:focus-visible&lt;/code&gt; on the container for keyboard focus ring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4: Keyboard event handling confusion
&lt;/h2&gt;

&lt;p&gt;Since keyboard events go to the focused container, your widget’s keyboard logic must listen on the container, not list items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Attach keyboard event handlers to the container and update &lt;code&gt;aria-activedescendant&lt;/code&gt; accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together: a concrete example
&lt;/h2&gt;

&lt;p&gt;Imagine a custom listbox implemented like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;&amp;lt;div role="listbox" tabindex="0" aria-activedescendant="item-3"&amp;gt;&lt;/code&gt; is the keyboard focus target.&lt;/li&gt;
&lt;li&gt;Inside are &lt;code&gt;&amp;lt;div role="option" id="item-1" tabindex="-1"&amp;gt;&lt;/code&gt; elements.&lt;/li&gt;
&lt;li&gt;User presses ArrowDown.&lt;/li&gt;
&lt;li&gt;Widget code updates &lt;code&gt;aria-activedescendant&lt;/code&gt; to "item-4", but does NOT call &lt;code&gt;.focus()&lt;/code&gt; on that option.&lt;/li&gt;
&lt;li&gt;The container remains focused, keyboard events continue to work, and screen readers announce the newly active item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern makes keyboard navigation smooth and accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why browsers chose this design
&lt;/h2&gt;

&lt;p&gt;Managing a single focused element reduces complexity for assistive tech and keeps keyboard event routing predictable.&lt;/p&gt;

&lt;p&gt;If every interactive item could be focused in the DOM, keyboard navigation would become chaotic in complex widgets.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aria-activedescendant&lt;/code&gt; lets you keep one focus anchor but still indicate selection changes inside a composite widget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you’re building custom widgets, understanding the interplay of &lt;code&gt;tabindex="-1"&lt;/code&gt; and &lt;code&gt;aria-activedescendant&lt;/code&gt; is a game changer.&lt;/p&gt;

&lt;p&gt;Make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The container is focusable and focused.&lt;/li&gt;
&lt;li&gt;Keyboard events are handled on the container.&lt;/li&gt;
&lt;li&gt;You update &lt;code&gt;aria-activedescendant&lt;/code&gt; to reflect the active item.&lt;/li&gt;
&lt;li&gt;You never move DOM focus to the active item itself.&lt;/li&gt;
&lt;li&gt;Visual highlight styles compensate for missing native focus ring on active descendants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this clarity, you’ll avoid subtle bugs, make your widgets friendlier to keyboard and screen reader users, and make debugging focus issues way less painful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/how-browsers-handle-keyboard-focus-navigation-with-tabindex-1-and-aria-activedescendant" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>keyboard</category>
      <category>focusmanagement</category>
      <category>aria</category>
    </item>
    <item>
      <title>Decoding the Accessibility Tree in Shadow DOM: Challenges and Solutions</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:48:17 +0000</pubDate>
      <link>https://dev.to/mspk97/decoding-the-accessibility-tree-in-shadow-dom-challenges-and-solutions-ahm</link>
      <guid>https://dev.to/mspk97/decoding-the-accessibility-tree-in-shadow-dom-challenges-and-solutions-ahm</guid>
      <description>&lt;p&gt;Ever had that moment where you build a shiny web component with Shadow DOM, test it in your browser, and everything looks perfect visually ,  but when you use a screen reader, your component feels like a black box? The screen reader either skips it entirely or reads confusing information.&lt;/p&gt;

&lt;p&gt;I hit this exact wall recently. My custom dropdown was gorgeous, encapsulated, and well-structured. But accessibility testing with VoiceOver and NVDA revealed a frustrating truth: the accessibility tree didn’t expose my component’s internals correctly. Why? Shadow DOM.&lt;/p&gt;

&lt;p&gt;Let me walk you through what’s really going on under the hood with the accessibility tree and Shadow DOM, why your screen reader might be getting lost in the shadows, and practical steps I took to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accessibility tree: your UI’s secret map
&lt;/h2&gt;

&lt;p&gt;You might be familiar with the DOM tree ,  the nested structure of HTML elements your browser renders. But assistive technologies don’t read the DOM directly. Instead, browsers build a parallel structure called the accessibility tree.&lt;/p&gt;

&lt;p&gt;This tree distills the UI into semantic roles, states, and properties meaningful to screen readers, magnifiers, and other AT. It merges HTML semantics, ARIA attributes, and some computed styles.&lt;/p&gt;

&lt;p&gt;When you press Tab or listen with a screen reader, you’re hearing the accessibility tree’s story, not the raw DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadow DOM: encapsulation’s double-edged sword
&lt;/h2&gt;

&lt;p&gt;Shadow DOM lets you build self-contained components. It hides implementation details, styles, and markup inside a shadow root, preventing conflicts and accidental overrides.&lt;/p&gt;

&lt;p&gt;But this encapsulation comes at a cost. The shadow root acts like a boundary that the browser’s accessibility tree builder must navigate carefully. Not every node inside a shadow root is necessarily exposed to assistive tech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Shadow DOM can break accessibility trees
&lt;/h2&gt;

&lt;p&gt;Here’s the kicker: when a browser constructs the accessibility tree, it treats shadow boundaries differently than light DOM. Some elements inside shadow roots may be omitted or flattened depending on browser heuristics and the component’s ARIA usage.&lt;/p&gt;

&lt;p&gt;For example, if your shadow root contains a button element but you don’t expose it properly, the screen reader may see just a generic container or nothing at all.&lt;/p&gt;

&lt;p&gt;Different browsers and screen readers handle this with subtle differences, making debugging extra tricky.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I debugged the shadow accessibility mess
&lt;/h2&gt;

&lt;p&gt;I started by inspecting the accessibility tree. Chrome DevTools has an Accessibility pane that shows the accessibility tree hierarchy. I compared what was inside my component’s shadow root to what was actually exposed.&lt;/p&gt;

&lt;p&gt;I found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The shadow root was represented as a node but its internals were missing or collapsed.&lt;/li&gt;
&lt;li&gt;ARIA roles I set inside the shadow root weren’t picked up.&lt;/li&gt;
&lt;li&gt;Keyboard focus was trapped or skipped unexpectedly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This told me the accessibility tree builder wasn’t bridging the shadow boundary properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategies to fix Shadow DOM accessibility
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Use &lt;code&gt;delegatesFocus&lt;/code&gt; when creating your shadow root
&lt;/h2&gt;

&lt;p&gt;When you create a shadow root with &lt;code&gt;element.attachShadow({ mode: 'open', delegatesFocus: true })&lt;/code&gt;, it allows the shadow DOM to delegate focus to internal elements. This helps keyboard users and screen readers interact with inner controls naturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Expose semantics explicitly with ARIA
&lt;/h2&gt;

&lt;p&gt;Inside your shadow DOM, use ARIA roles, properties, and states carefully. For example, if your shadow root wraps a listbox, mark the container with &lt;code&gt;role="listbox"&lt;/code&gt; and internal items with &lt;code&gt;role="option"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But remember: ARIA can’t fix everything. It’s better to use native semantics where possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use &lt;code&gt;aria-hidden&lt;/code&gt; and &lt;code&gt;tabindex&lt;/code&gt; strategically
&lt;/h2&gt;

&lt;p&gt;Sometimes, elements inside the shadow root should be hidden from the accessibility tree (like decorative wrappers). Mark them with &lt;code&gt;aria-hidden="true"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, ensure only interactive elements inside the shadow root have &lt;code&gt;tabindex="0"&lt;/code&gt; or greater, so focus doesn’t get lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Flatten the accessibility tree with &lt;code&gt;::part&lt;/code&gt; and &lt;code&gt;::slotted&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;::part&lt;/code&gt; and &lt;code&gt;::slotted&lt;/code&gt; pseudo-elements let you expose parts of your shadow DOM to styling and accessibility.&lt;/p&gt;

&lt;p&gt;By exposing interactive parts via &lt;code&gt;part&lt;/code&gt;, you let assistive tech recognize them more easily. Slotted content from light DOM is incorporated in the accessibility tree too.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test across browsers and screen readers
&lt;/h2&gt;

&lt;p&gt;Accessibility tree handling varies. Test with Chrome + NVDA, Safari + VoiceOver, Firefox + Orca, etc. Tools like the Accessibility Insights browser extension help visualize the accessibility tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example: making a shadow dropdown accessible
&lt;/h2&gt;

&lt;p&gt;Here’s a simplified pattern I used for a dropdown inside Shadow DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachShadow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delegatesFocus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  &amp;lt;div role="listbox" tabindex="0"&amp;gt;
    &amp;lt;div role="option" tabindex="-1"&amp;gt;Option 1&amp;lt;/div&amp;gt;
    &amp;lt;div role="option" tabindex="-1"&amp;gt;Option 2&amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;listbox&lt;/code&gt; container is focusable and has the right role.&lt;/li&gt;
&lt;li&gt;Each option is marked with &lt;code&gt;role="option"&lt;/code&gt; and is not focusable by tab (tabindex="-1") but focus is managed programmatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup showed up correctly in the accessibility tree and the screen reader announced options as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final tip: Shadow DOM doesn’t have to be an accessibility black hole
&lt;/h2&gt;

&lt;p&gt;Shadow DOM can feel like a barrier to accessibility, but with some care, you can build components that communicate clearly with assistive tech.&lt;/p&gt;

&lt;p&gt;Use browser devtools to peek at the accessibility tree, leverage &lt;code&gt;delegatesFocus&lt;/code&gt;, ARIA roles, and test with real screen readers early and often.&lt;/p&gt;

&lt;p&gt;Once you decode how shadow boundaries shape the accessibility tree, you’ll avoid surprises and build components everyone can use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/decoding-the-accessibility-tree-in-shadow-dom-challenges-and-solutions" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>shadowdom</category>
      <category>screenreaders</category>
      <category>webcomponents</category>
    </item>
    <item>
      <title>AI-Powered Frontend Debugging Tools: How They Work, What They Fix, and When to Watch Out</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Thu, 17 Sep 2026 10:41:57 +0000</pubDate>
      <link>https://dev.to/mspk97/ai-powered-frontend-debugging-tools-how-they-work-what-they-fix-and-when-to-watch-out-2p7p</link>
      <guid>https://dev.to/mspk97/ai-powered-frontend-debugging-tools-how-they-work-what-they-fix-and-when-to-watch-out-2p7p</guid>
      <description>&lt;p&gt;Ever been stuck on a frontend bug that just wouldn’t quit? You try console.logs, step through the code, and maybe even rubber-duck your way through it. Then you hear about AI-powered debugging tools that claim to read your code, inspect live data, and suggest fixes. Sounds magical, right? &lt;/p&gt;

&lt;p&gt;I recently took a deep dive into how these AI assistants actually work under the hood, what makes them useful, and the pitfalls that caught me off guard. Spoiler: they can speed up your debugging but can also lead you into traps if you don’t keep your wits about you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The familiar frustration: Debugging frontend bugs is often a guessing game
&lt;/h2&gt;

&lt;p&gt;Imagine you have a React app where a button click sometimes fails silently. No errors in the console, no obvious clues. You add console.logs, inspect props, trace event handlers ,  but the cause remains elusive.&lt;/p&gt;

&lt;p&gt;This exact scenario is where AI debugging tools shine. They promise to analyze your code, runtime state, and sometimes even network requests to offer targeted hints or code fixes.&lt;/p&gt;

&lt;p&gt;But how do they do it?&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI debugging assistants peek under the hood
&lt;/h2&gt;

&lt;p&gt;At their core, these tools combine two main things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code understanding&lt;/strong&gt;: They parse your source files (JavaScript, JSX, CSS) to build a model of your app’s structure, functions, and data flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Runtime data analysis&lt;/strong&gt;: They hook into your app as it runs ,  collecting error logs, inspecting variable values, event traces, or even performance stats.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then they feed all this into a large language model (LLM), like GPT-4 or specialized code models, prompting it with your code snippets and runtime context.&lt;/p&gt;

&lt;p&gt;The AI tries to figure out what the bug might be and suggests fixes or debugging steps. Some tools go further and generate patch diffs or automated tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concrete example: Debugging a mysterious state update bug
&lt;/h2&gt;

&lt;p&gt;I tested one tool on a React component where a state update didn’t trigger a re-render. The AI assistant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsed the component code to understand state declarations and effects.&lt;/li&gt;
&lt;li&gt;Looked at console logs and event handlers.&lt;/li&gt;
&lt;li&gt;Suggested that a state variable was mutated directly instead of replaced, causing React to skip the re-render.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The suggestion included a code snippet showing how to create a new state object instead of mutating the existing one. That was exactly the bug.&lt;/p&gt;

&lt;p&gt;This saved me a few rounds of trial-and-error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes these AI tools helpful?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context awareness&lt;/strong&gt;: Unlike generic Stack Overflow searches, they see your exact code and runtime environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive debugging&lt;/strong&gt;: Some let you ask follow-up questions or request code explanations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: They can generate hypotheses faster than manual debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning aid&lt;/strong&gt;: Seeing AI’s reasoning can teach you new techniques or API usages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  But watch out: AI debugging isn’t foolproof
&lt;/h2&gt;

&lt;p&gt;AI models hallucinate ,  they sometimes confidently suggest fixes that don’t actually work or misinterpret the code context.&lt;/p&gt;

&lt;p&gt;In one case, the assistant suggested adding a missing import that wasn’t actually missing, which would have caused a new error.&lt;/p&gt;

&lt;p&gt;Also, many AI tools have limited visibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They don’t truly run your code in a debugger, so can’t catch runtime side effects reliably.&lt;/li&gt;
&lt;li&gt;They may lack access to complete app state or external APIs.&lt;/li&gt;
&lt;li&gt;They sometimes treat symptoms, not root causes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Blindly applying AI-generated fixes can introduce subtle bugs or security risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get the most out of AI debugging assistants
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use their suggestions as hypotheses, not gospel truth. Always review and test.&lt;/li&gt;
&lt;li&gt;Combine AI insights with traditional debugging tools ,  breakpoints, performance profilers, network inspectors.&lt;/li&gt;
&lt;li&gt;Feed the AI rich context: include relevant code snippets, error messages, and runtime logs.&lt;/li&gt;
&lt;li&gt;Be wary of fixes that feel too good or too generic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this means for your developer workflow
&lt;/h2&gt;

&lt;p&gt;AI debugging tools can become a handy pair of extra eyes ,  especially for tricky frontend bugs where code and runtime state interplay is complex.&lt;/p&gt;

&lt;p&gt;They won’t replace your intuition or understanding but can speed up the cycle of forming and testing hypotheses.&lt;/p&gt;

&lt;p&gt;As these tools evolve, expect tighter integration with editors, browsers, and CI pipelines, making debugging more interactive and data-driven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I’m excited about AI’s potential to tame frontend bugs, but I’m also cautious. These assistants are powerful new teammates but not omniscient gurus.&lt;/p&gt;

&lt;p&gt;Keep your debugging skills sharp, test everything thoroughly, and use AI suggestions to supplement, not replace, your developer judgment. That’s how you’ll turn AI debugging tools from a neat novelty into a trusted part of your toolbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/ai-powered-frontend-debugging-tools-how-they-work-what-they-fix-and-when-to-watch-out" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>debugging</category>
      <category>frontend</category>
      <category>developertools</category>
    </item>
    <item>
      <title>ARIA Role Inheritance and Override: How Browsers Resolve Conflicting Semantics</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Wed, 26 Aug 2026 13:37:59 +0000</pubDate>
      <link>https://dev.to/mspk97/aria-role-inheritance-and-override-how-browsers-resolve-conflicting-semantics-25oi</link>
      <guid>https://dev.to/mspk97/aria-role-inheritance-and-override-how-browsers-resolve-conflicting-semantics-25oi</guid>
      <description>&lt;p&gt;Ever had that moment where you slap an ARIA role on an element, only to find your screen reader ignores it or reads something totally unexpected? You double-check your markup, and then notice you also have some nested roles, or conflicting aria attributes nearby. What’s going on?&lt;/p&gt;

&lt;p&gt;I ran into this while fixing accessibility bugs on a client site. A button inside a custom widget had role="button" but also aria-haspopup="menu" ,  and the screen reader would announce "menu" instead of "button." Digging in, I realized that the way browsers handle ARIA roles and properties isn’t just about what you explicitly set on an element. There’s a whole system of inheritance, overrides, and conflict resolution happening under the hood.&lt;/p&gt;

&lt;p&gt;Let’s talk about how browsers actually resolve ARIA roles and properties when there are conflicts, and what that means for you when debugging accessibility issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tangled web of ARIA roles and properties
&lt;/h2&gt;

&lt;p&gt;ARIA roles describe what an element &lt;em&gt;is&lt;/em&gt; to assistive technology. For example, &lt;code&gt;role="button"&lt;/code&gt; tells a screen reader this element behaves like a button. Other roles include &lt;code&gt;menu&lt;/code&gt;, &lt;code&gt;checkbox&lt;/code&gt;, &lt;code&gt;dialog&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;But ARIA also has properties and states, like &lt;code&gt;aria-haspopup&lt;/code&gt;, &lt;code&gt;aria-checked&lt;/code&gt;, or &lt;code&gt;aria-expanded&lt;/code&gt;, that modify or add semantic information.&lt;/p&gt;

&lt;p&gt;The confusion starts when you put multiple roles or conflicting properties on the same element, or when nested elements define roles that might clash.&lt;/p&gt;

&lt;p&gt;Here’s the kicker: &lt;strong&gt;an element can only have one computed role&lt;/strong&gt;. While you can set multiple roles explicitly in your markup (like via &lt;code&gt;role="button menu"&lt;/code&gt;, which is invalid), browsers use a priority and inheritance system to pick one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How browsers pick the winning ARIA role
&lt;/h2&gt;

&lt;p&gt;The spec gives some guidelines, but browser implementations differ subtly. Here’s the gist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit role attribute wins:&lt;/strong&gt; If you set &lt;code&gt;role&lt;/code&gt; on an element, that role is the first candidate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implicit role fallback:&lt;/strong&gt; If no explicit role is set, browsers may infer a role based on the tag name and attributes (like &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; implicitly has role="button").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Special roles override generic ones:&lt;/strong&gt; Some roles have higher precedence. For example, &lt;code&gt;alert&lt;/code&gt; overrides &lt;code&gt;region&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARIA role inheritance:&lt;/strong&gt; Roles do not literally inherit like CSS properties, but some ARIA properties do affect descendants’ semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if you have &lt;code&gt;role="menu"&lt;/code&gt; on a container and inside it a child with &lt;code&gt;role="menuitem"&lt;/code&gt;, the child’s role is explicit and independent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about conflicting ARIA properties?
&lt;/h2&gt;

&lt;p&gt;This is where things get tricky. Properties like &lt;code&gt;aria-haspopup&lt;/code&gt; can influence what screen readers announce.&lt;/p&gt;

&lt;p&gt;In my example, adding &lt;code&gt;aria-haspopup="menu"&lt;/code&gt; to a button tells the screen reader the button controls a menu ,  sometimes causing it to announce "menu" or "button with menu." But if you also set &lt;code&gt;role="menu"&lt;/code&gt; on the same element, the role wins and it treats the element as a menu, not a button.&lt;/p&gt;

&lt;p&gt;In other cases, properties can override or augment roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aria-checked&lt;/code&gt; on a checkbox or switch controls the checked state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-expanded&lt;/code&gt; on a disclosure widget signals whether it’s open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if the role and the ARIA properties don’t align, screen readers may behave unpredictably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood: the accessibility tree and role resolution
&lt;/h2&gt;

&lt;p&gt;The secret sauce is the accessibility tree ,  a parallel structure browsers build from the DOM, CSS, and ARIA attributes that assistive tech consumes.&lt;/p&gt;

&lt;p&gt;When the browser parses the DOM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It looks for explicit roles on elements.&lt;/li&gt;
&lt;li&gt;If absent, it infers implicit roles.&lt;/li&gt;
&lt;li&gt;It collects ARIA properties, validating them against the role.&lt;/li&gt;
&lt;li&gt;It resolves conflicts:

&lt;ul&gt;
&lt;li&gt;Multiple roles aren’t allowed; the browser picks one by priority or last-known override.&lt;/li&gt;
&lt;li&gt;Conflicting properties may be ignored or cause fallback behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process can differ slightly between browsers and screen readers, which is why your accessibility testing must include multiple platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging tips: How to see the computed ARIA role and properties
&lt;/h2&gt;

&lt;p&gt;Feeling lost? Here’s how to peek under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use browser devtools accessibility pane:&lt;/strong&gt; Chrome and Firefox have accessibility inspectors that show the computed role, name, states, and properties for any element.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VoiceOver or NVDA output:&lt;/strong&gt; Listen carefully for what role and state the screen reader announces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility tree snapshots:&lt;/strong&gt; Tools like the Accessibility Object Model (AOM) inspector or axe-core can reveal discrepancies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, in Chrome DevTools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Right-click your element and inspect.&lt;/li&gt;
&lt;li&gt;Go to the "Accessibility" tab.&lt;/li&gt;
&lt;li&gt;See the "Computed Properties" ,  it shows the effective role and ARIA states.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the computed role isn’t what you expect, check for conflicting &lt;code&gt;role&lt;/code&gt; attributes on the same or ancestor elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls and how to avoid them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don’t mix roles on the same element:&lt;/strong&gt; Stick to one explicit &lt;code&gt;role&lt;/code&gt;. If you need nested roles, use child elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Align ARIA properties with the role:&lt;/strong&gt; Don’t add &lt;code&gt;aria-haspopup="menu"&lt;/code&gt; on an element with &lt;code&gt;role="menu"&lt;/code&gt;; it’s redundant and confusing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use native semantics when possible:&lt;/strong&gt; Native HTML elements have implicit roles and states that assistive tech understands well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with multiple screen readers:&lt;/strong&gt; Behavior varies, so verify your ARIA usage under real conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why does this matter?
&lt;/h2&gt;

&lt;p&gt;Misusing ARIA roles and properties can confuse users relying on assistive technology. Overriding or conflicting roles may cause screen readers to announce the wrong widget type, breaking navigation and interaction.&lt;/p&gt;

&lt;p&gt;Understanding how browsers resolve these conflicts helps you write cleaner, more predictable accessible code.&lt;/p&gt;

&lt;p&gt;Next time your ARIA role isn’t behaving as expected, remember it’s not just the markup but how the browser builds the accessibility tree and resolves those roles and properties. Use the tools to inspect what’s really going on, and keep your roles clear and consistent.&lt;/p&gt;

&lt;p&gt;Accessibility isn’t just about adding attributes ,  it’s about making sure those attributes speak clearly and consistently to the user’s assistive technology.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/aria-role-inheritance-and-override-how-browsers-resolve-conflicting-semantics" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>aria</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Optimizing Keyboard Navigation in Complex Web Apps: Techniques and Debugging Tools</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Tue, 25 Aug 2026 13:03:52 +0000</pubDate>
      <link>https://dev.to/mspk97/optimizing-keyboard-navigation-in-complex-web-apps-techniques-and-debugging-tools-2kla</link>
      <guid>https://dev.to/mspk97/optimizing-keyboard-navigation-in-complex-web-apps-techniques-and-debugging-tools-2kla</guid>
      <description>&lt;p&gt;Ever found yourself tabbing through a complex web app only to get trapped inside a modal, or worse, see the focus jump erratically like it’s playing hide and seek? I’ve been there, building custom widgets that looked sleek but turned into nightmares for keyboard users.&lt;/p&gt;

&lt;p&gt;It’s one thing to have basic tab order working. It’s another story entirely when you have nested widgets, dynamic content, or custom keyboard shortcuts. You want your app to feel smooth and predictable for keyboard users, but the reality is often a tangled mess of tabindexes, event handlers, and accessibility quirks.&lt;/p&gt;

&lt;p&gt;In this post, I’ll share what I learned about optimizing keyboard navigation in complex web apps. We’ll dive into techniques for managing focus order, handling keyboard events gracefully, and using browser and screen reader tools to track down the root causes of weird focus or event issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Keyboard Navigation Gets Messy
&lt;/h2&gt;

&lt;p&gt;Picture a custom dropdown inside a modal that itself lives inside a tabbed interface. You press Tab expecting to move logically through the UI, but suddenly the focus skips the dropdown’s toggle or gets stuck inside the modal with no way out.&lt;/p&gt;

&lt;p&gt;Why does this happen?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incorrect tabindex values&lt;/strong&gt;: Misusing or overusing tabindex can break natural tab flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No focus management on dynamic UI&lt;/strong&gt;: When widgets open or close, focus should move appropriately, but often it doesn’t.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard event conflicts&lt;/strong&gt;: Overlapping handlers steal or swallow keys like Escape or Arrow keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen readers and assistive tech nuances&lt;/strong&gt;: What looks fine visually might confuse screen readers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I recently ran into this while building a complex combo box. The toggle button was focusable, but pressing Down Arrow didn’t move focus to the list items as expected. Worse, closing the list didn’t return focus to the toggle button. Keyboard users were left puzzled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Principles Under the Hood
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Focusability: What Can Receive Focus?
&lt;/h2&gt;

&lt;p&gt;Native interactive elements like &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; are focusable by default. Non-interactive elements like &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; are not unless you add &lt;code&gt;tabindex="0"&lt;/code&gt; or &lt;code&gt;tabindex="-1"&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tabindex="0"&lt;/code&gt; makes an element keyboard focusable in natural tab order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tabindex="-1"&lt;/code&gt; makes an element focusable only programmatically (e.g., via &lt;code&gt;.focus()&lt;/code&gt;) but skipped in tab order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using tabindex incorrectly often leads to confusing tab sequences or focus traps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Focus on State Changes
&lt;/h2&gt;

&lt;p&gt;When you open a dropdown or modal, you should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move focus into the widget (usually to the first interactive element inside).&lt;/li&gt;
&lt;li&gt;Trap focus within the widget if appropriate (e.g., modals).&lt;/li&gt;
&lt;li&gt;Restore focus to the originating element when closing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing this manually means calling &lt;code&gt;.focus()&lt;/code&gt; at the right time and managing keyboard event handlers to trap or release focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyboard Event Handling
&lt;/h2&gt;

&lt;p&gt;Keyboard events come in three flavors: &lt;code&gt;keydown&lt;/code&gt;, &lt;code&gt;keypress&lt;/code&gt;, and &lt;code&gt;keyup&lt;/code&gt;. For custom navigation, &lt;code&gt;keydown&lt;/code&gt; is typically the best place to intercept keys because it fires earliest.&lt;/p&gt;

&lt;p&gt;Be careful to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevent default browser behavior only when necessary.&lt;/li&gt;
&lt;li&gt;Avoid conflicts with other handlers.&lt;/li&gt;
&lt;li&gt;Respect modifier keys like Shift or Alt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, intercepting Arrow keys to move focus inside a list requires preventing default scrolling but letting other keys pass through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools to See What’s Really Happening
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Browser DevTools - Focus Debugging
&lt;/h2&gt;

&lt;p&gt;Modern browsers have features to inspect focus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elements panel&lt;/strong&gt;: Inspect the currently focused element with &lt;code&gt;document.activeElement&lt;/code&gt; in the console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility pane&lt;/strong&gt;: See what the browser exposes to assistive tech.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab order visualization&lt;/strong&gt;: Some browsers or extensions highlight tabbable elements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try this in Chrome’s console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or even add event listeners to log focus changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;focusin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus moved to:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps you verify where focus lands as you tab around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screen Reader Testing
&lt;/h2&gt;

&lt;p&gt;Screen readers like NVDA (Windows), VoiceOver (macOS), or Narrator (Windows) expose how your app’s focus and roles are announced.&lt;/p&gt;

&lt;p&gt;Turn on your screen reader and:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tab through your app.&lt;/li&gt;
&lt;li&gt;Listen for announcements.&lt;/li&gt;
&lt;li&gt;Verify the focus highlights match what’s announced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Discrepancies here often reveal mismatches between &lt;code&gt;aria-*&lt;/code&gt; attributes and actual focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyboard Event Listeners in Debug Mode
&lt;/h2&gt;

&lt;p&gt;Adding verbose logging on keyboard events can clarify which handlers run and in what order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Key down:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Target:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this to detect if some handler is calling &lt;code&gt;e.preventDefault()&lt;/code&gt; too early or swallowing events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Techniques to Fix Common Issues
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Prefer Native Elements When Possible
&lt;/h2&gt;

&lt;p&gt;Buttons, links, inputs come with built-in keyboard support. When you need custom widgets, start from a native element or at least use &lt;code&gt;role&lt;/code&gt; and &lt;code&gt;tabindex&lt;/code&gt; carefully.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use &lt;code&gt;tabindex&lt;/code&gt; Sparingly and Correctly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Avoid positive tabindex values (e.g., &lt;code&gt;tabindex="1"&lt;/code&gt;) as they reorder tabbing unpredictably.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;tabindex="0"&lt;/code&gt; for elements that must be focusable in order.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;tabindex="-1"&lt;/code&gt; for programmatic focus targets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Implement Focus Management on Open/Close
&lt;/h2&gt;

&lt;p&gt;When opening a dropdown or modal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openDropdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dropdownElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;dropdownElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;li&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When closing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;closeDropdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dropdownElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;toggleButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Trap Focus Inside Modals
&lt;/h2&gt;

&lt;p&gt;Trap focus by listening to &lt;code&gt;keydown&lt;/code&gt; for Tab and Shift+Tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// logic to cycle focus inside modal&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Libraries like &lt;code&gt;focus-trap&lt;/code&gt; automate this.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Handle Arrow Keys and Other Navigation Keys
&lt;/h2&gt;

&lt;p&gt;For list widgets, intercept Up/Down arrows to move focus between items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onKeyDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ArrowDown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;moveFocusToNextItem&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to update &lt;code&gt;aria-activedescendant&lt;/code&gt; or focus accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrangling a Real Bug: A Case Study
&lt;/h2&gt;

&lt;p&gt;In my combo box, pressing Down Arrow didn’t move focus to the list items. Turns out I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The toggle button focusable with &lt;code&gt;tabindex="0"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The list container with &lt;code&gt;tabindex="-1"&lt;/code&gt; but no focus shifting on open.&lt;/li&gt;
&lt;li&gt;Keyboard handler on the toggle button that didn’t prevent default or move focus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added code to focus the first list item on open.&lt;/li&gt;
&lt;li&gt;Prevented default on Arrow Down.&lt;/li&gt;
&lt;li&gt;Managed &lt;code&gt;aria-expanded&lt;/code&gt; and &lt;code&gt;aria-controls&lt;/code&gt; attributes properly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once fixed, keyboard navigation felt natural and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Keyboard navigation in complex apps is tricky because you’re fighting the browser’s natural tab order, your app’s dynamic UI, and the expectations of assistive tech users.&lt;/p&gt;

&lt;p&gt;By understanding focusability, carefully managing focus on UI state changes, and using browser and screen reader tools for debugging, you can make keyboard navigation smooth and reliable.&lt;/p&gt;

&lt;p&gt;Next time your keyboard users get lost or stuck, you’ll have a better idea of where to look and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/optimizing-keyboard-navigation-in-complex-web-apps-techniques-and-debugging-tools" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>CSS Container Queries: How Browsers Calculate and Recalculate Styles</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Mon, 24 Aug 2026 10:38:55 +0000</pubDate>
      <link>https://dev.to/mspk97/css-container-queries-how-browsers-calculate-and-recalculate-styles-kk7</link>
      <guid>https://dev.to/mspk97/css-container-queries-how-browsers-calculate-and-recalculate-styles-kk7</guid>
      <description>&lt;p&gt;Ever had a layout break because a component looked perfect on your screen but warped on a colleague’s? Or tried to build a responsive widget that adapts to its container ,  not just the viewport ,  only to realize CSS media queries don’t cut it?&lt;/p&gt;

&lt;p&gt;That’s exactly where CSS container queries come in. They let you apply styles based on the size of a container element instead of the whole viewport. It feels like magic until you hit bugs or weird reflows and start wondering: how exactly do browsers figure out when to apply those styles? How do they know the container size changed, and what happens next?&lt;/p&gt;

&lt;p&gt;I spent a few weeks poking around browser engines and experimenting with container queries in real projects. Here’s what I learned about the mechanics under the hood, how browsers detect container resizes, what triggers style recalculations, and how to debug tricky issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  When container queries hit the scene
&lt;/h2&gt;

&lt;p&gt;Imagine you have a card component inside a sidebar. The sidebar can be narrow or wide. You want the card’s content to switch layout based on the card’s actual width ,  not the viewport width.&lt;/p&gt;

&lt;p&gt;Previously, this was a headache. You’d either need JavaScript resize listeners or complex hacks. Now, with container queries, you write something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect. But how does the browser know when the card’s size crosses that 300px threshold?&lt;/p&gt;

&lt;h2&gt;
  
  
  The container query workflow inside browsers
&lt;/h2&gt;

&lt;p&gt;At a high level, browsers treat container queries as a new kind of style condition. But unlike media queries ,  which depend on the viewport or device ,  container queries depend on the size of the container element itself.&lt;/p&gt;

&lt;p&gt;Here’s the rough flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Container detection:&lt;/strong&gt; The browser looks for elements styled with &lt;code&gt;container-type&lt;/code&gt;. This tells it which elements are containers and what dimension (inline-size, block-size, or both) to watch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Size measurement:&lt;/strong&gt; On layout, the browser measures those container elements’ dimensions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Style matching:&lt;/strong&gt; When applying styles, the browser evaluates &lt;code&gt;@container&lt;/code&gt; rules using the measured container size instead of viewport size.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Style recalculation:&lt;/strong&gt; If the container size changes, the browser reevaluates container queries for all descendants that rely on that container.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Layout and paint:&lt;/strong&gt; If styles changed, it triggers layout and paint updates as usual.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How does the browser detect container size changes?
&lt;/h2&gt;

&lt;p&gt;This is the trickiest part. Browsers don’t constantly measure every container element on every frame ,  that would tank performance.&lt;/p&gt;

&lt;p&gt;Instead, they rely on a combination of mechanisms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intrinsic size changes:&lt;/strong&gt; If the container’s content or style changes in a way that affects its size, the layout engine notices during its normal flow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit resize observation:&lt;/strong&gt; Browsers implement something similar to the Resize Observer API internally to detect size changes on container elements. When a container’s size changes, the browser marks it as needing container query reevaluation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cached sizes:&lt;/strong&gt; To avoid thrashing, browsers cache container sizes and only trigger container query reevaluation if sizes actually differ beyond subpixel rounding.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means if your container’s width changes from 290.4px to 310.1px, that crossing of the 300px threshold triggers new container query matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when a container size changes?
&lt;/h2&gt;

&lt;p&gt;When a container’s size changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The browser schedules a &lt;strong&gt;style recalculation&lt;/strong&gt; focused on container queries inside that container.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It reevaluates any &lt;code&gt;@container&lt;/code&gt; rules whose conditions depend on that container.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If styles for some elements change, it triggers a &lt;strong&gt;layout update&lt;/strong&gt; for those elements and their children.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, it paints the updated parts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since container queries can cascade, a single container size change might ripple to multiple nested containers, but browsers optimize to avoid unnecessary recalculations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood: container queries and the rendering pipeline
&lt;/h2&gt;

&lt;p&gt;Container queries introduce a dependency in the style system that looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Container size → style rules → layout → paint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This dependency means the browser’s rendering pipeline adds a new observation step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;During the &lt;strong&gt;style calculation phase&lt;/strong&gt;, container sizes are used to pick styles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If container sizes change, style recalculation happens, which can cascade to layout.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a subtle change compared to media queries because container queries depend on &lt;em&gt;dynamic element size&lt;/em&gt; instead of global viewport size, which can change as a side effect of style or layout itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world debugging tips for container queries
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Check if your container actually has &lt;code&gt;container-type&lt;/code&gt; set
&lt;/h2&gt;

&lt;p&gt;Without this property, container queries silently don’t work. It’s not enough that you have &lt;code&gt;@container&lt;/code&gt; rules ,  the container element must declare itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Verify the container’s size
&lt;/h2&gt;

&lt;p&gt;Use DevTools to inspect the container’s box size. Sometimes padding, borders, or box-sizing cause the size to be different than you expect.&lt;/p&gt;

&lt;p&gt;Resize your container manually (e.g., by resizing a pane or window) and watch if styles update.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Understand container sizing modes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;container-type&lt;/code&gt; can be &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;inline-size&lt;/code&gt;, or &lt;code&gt;normal&lt;/code&gt;. The most common is &lt;code&gt;inline-size&lt;/code&gt;, which watches width in horizontal writing modes.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;@container&lt;/code&gt; queries use block-size conditions but your container only reports inline-size, those queries won’t match.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Beware of nested containers
&lt;/h2&gt;

&lt;p&gt;If you have multiple nested containers, each with their own queries, the browser tracks each container separately. Styles inside nested containers can re-trigger layout, so be mindful of complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Watch out for layout thrashing
&lt;/h2&gt;

&lt;p&gt;If container queries cause style changes that affect container sizes themselves, you can get layout loops or jank.&lt;/p&gt;

&lt;p&gt;Try to avoid container query rules that increase container size when the container size is already near your breakpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use Resize Observer for debugging
&lt;/h2&gt;

&lt;p&gt;You can also add a &lt;code&gt;ResizeObserver&lt;/code&gt; in your JS to watch container sizes and log when they change. This helps confirm when the browser detects size changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResizeObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Container size changed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What’s next with container queries?
&lt;/h2&gt;

&lt;p&gt;Browser support is solid and growing, but some quirks remain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Container queries don’t yet work inside &lt;code&gt;iframe&lt;/code&gt;s or shadow DOM in all browsers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performance is still evolving. Benchmark your layouts if you use many or deeply nested containers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The spec may add more features like container query lists or interaction-based queries.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Container queries feel like a breath of fresh air for responsive design that adapts to component size, not just viewport size. But under the hood, they add a new dimension to the browser’s rendering pipeline ,  tracking container sizes, recalculating styles dynamically, and triggering layout updates selectively.&lt;/p&gt;

&lt;p&gt;Understanding how browsers detect container size changes and apply container query styles helps you avoid surprises and debug issues faster.&lt;/p&gt;

&lt;p&gt;Next time your card flips layout on a size boundary, you’ll know exactly what’s going on inside the browser to make that happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/css-container-queries-how-browsers-calculate-and-recalculate-styles" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>css</category>
      <category>frontend</category>
      <category>webdev</category>
      <category>rendering</category>
    </item>
    <item>
      <title>React Portals: How They Work and When to Use Them for Modals and Tooltips</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:11:08 +0000</pubDate>
      <link>https://dev.to/mspk97/react-portals-how-they-work-and-when-to-use-them-for-modals-and-tooltips-32o4</link>
      <guid>https://dev.to/mspk97/react-portals-how-they-work-and-when-to-use-them-for-modals-and-tooltips-32o4</guid>
      <description>&lt;p&gt;You’ve probably built a modal or tooltip in React and hit a weird snag: the overlay appears visually outside your usual component tree, but events don’t behave as you expect. Maybe clicks inside the modal don’t bubble the way you thought, or keyboard focus seems lost when you open it.&lt;/p&gt;

&lt;p&gt;That’s React portals messing with your head ,  in a good way. They let you render children into a DOM node outside your React root, which is great for modals, tooltips, dropdowns, and other overlays. But under the hood, portals change the event propagation and focus flow in subtle ways.&lt;/p&gt;

&lt;p&gt;I ran into this recently while debugging a modal that wouldn’t close on outside clicks. The culprit? A misunderstanding of how portals handle events and focus in the DOM.&lt;/p&gt;

&lt;p&gt;Let me walk you through how React portals work, what happens to events and focus, and some practical debugging tips when using portals for modals and tooltips.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Exactly Is a React Portal?
&lt;/h2&gt;

&lt;p&gt;React portals are an escape hatch. Normally, React renders your component tree inside a single root DOM node ,  often &lt;code&gt;&amp;lt;div id="root"&amp;gt;&lt;/code&gt;. But what if you want a component somewhere else in the DOM, like a modal appended to &lt;code&gt;document.body&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Portals let you do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells React: render &lt;code&gt;children&lt;/code&gt; into DOM &lt;code&gt;container&lt;/code&gt;, outside the main React DOM tree.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modals and tooltips often need to break out of parent containers that may have &lt;code&gt;overflow: hidden&lt;/code&gt; or &lt;code&gt;z-index&lt;/code&gt; stacking contexts.&lt;/li&gt;
&lt;li&gt;They need to sit at a higher DOM level for proper layering and positioning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So portals let you keep React’s declarative component model while placing DOM nodes where they need to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens Under the Hood?
&lt;/h2&gt;

&lt;p&gt;React internally keeps track of the portal’s target container. It renders the portal’s children into that container as real DOM nodes.&lt;/p&gt;

&lt;p&gt;But React’s component tree and the DOM tree then diverge. Your React tree says "Modal is a child of App," but the DOM says "Modal is a child of &lt;code&gt;document.body&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;This divergence is the core of what trips people up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Propagation Isn’t Broken ,  But It’s Different
&lt;/h2&gt;

&lt;p&gt;Here’s the key: React uses a single event delegation system at the root container. Events bubble through the DOM, then React maps them back to components.&lt;/p&gt;

&lt;p&gt;With portals, the DOM tree is split. The portal’s DOM nodes live somewhere else, outside the root container where React listens for events.&lt;/p&gt;

&lt;p&gt;So how does React handle events from portal nodes?&lt;/p&gt;

&lt;p&gt;React attaches event listeners at the root of each React render tree ,  and for portals, it attaches them at the portal’s container node too.&lt;/p&gt;

&lt;p&gt;This means events inside a portal bubble through the portal container, then propagate to the React root container.&lt;/p&gt;

&lt;p&gt;In practice, this usually works seamlessly, but there are some event propagation edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Events that rely on the DOM tree hierarchy (like native event bubbling) behave normally because the portal nodes are real DOM children of the portal container.&lt;/li&gt;
&lt;li&gt;But React’s synthetic event system treats portal containers as roots for event delegation, so some event listeners might fire in different orders.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This explains why outside click detection on portals sometimes fails if you’re relying on event bubbling inside the React tree without accounting for the portal’s container boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Focus Management: The Invisible Trap
&lt;/h2&gt;

&lt;p&gt;Modals and tooltips often trap focus for accessibility: when the modal opens, keyboard focus should move inside it, and tabbing should cycle within the modal.&lt;/p&gt;

&lt;p&gt;Portals complicate this because the modal’s DOM nodes are outside the React root and its focus context.&lt;/p&gt;

&lt;p&gt;Browsers don’t care about React internals ,  they only see DOM nodes. So focus will move normally, but your app’s logic to restore focus or detect focused elements needs to account for the portal container.&lt;/p&gt;

&lt;p&gt;For example, if you try to detect clicks outside a modal by checking if the event target is inside your React tree, it won’t work ,  because the modal lives in a separate DOM subtree.&lt;/p&gt;

&lt;p&gt;Also, keyboard navigation and focus outlines might behave unexpectedly if CSS styles or focus management code assume the modal is nested inside the React root.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Debugging Tips for Portal-Based Modals and Tooltips
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Use &lt;code&gt;ref&lt;/code&gt; on the Portal Container
&lt;/h2&gt;

&lt;p&gt;Keep a ref to the portal container DOM node (&lt;code&gt;document.body&lt;/code&gt; or a div you created). When handling clicks or focus events, check if the event target is inside that container using &lt;code&gt;container.contains(event.target)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is more reliable than React tree checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Attach Event Listeners on the Portal Container
&lt;/h2&gt;

&lt;p&gt;For outside click detection, attach mouse event handlers on the portal container or even &lt;code&gt;document&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;React’s synthetic events may not catch all events correctly when portals are involved, so sometimes native event listeners are more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Manage Focus Explicitly
&lt;/h2&gt;

&lt;p&gt;When opening a modal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focus the first focusable element inside the portal.&lt;/li&gt;
&lt;li&gt;Trap tab focus inside the portal container.&lt;/li&gt;
&lt;li&gt;Return focus to the opener element when closing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use libraries like &lt;code&gt;focus-trap-react&lt;/code&gt; or build your own with careful DOM traversal.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Watch for CSS Stacking Context Issues
&lt;/h2&gt;

&lt;p&gt;Since portals render outside your main tree, your modal or tooltip might be affected by &lt;code&gt;z-index&lt;/code&gt;, &lt;code&gt;position&lt;/code&gt;, or &lt;code&gt;overflow&lt;/code&gt; styles on ancestor nodes.&lt;/p&gt;

&lt;p&gt;Debug with DevTools by inspecting the portal container and ancestors to ensure it’s visible and on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Beware of Event Ordering Differences
&lt;/h2&gt;

&lt;p&gt;If you have both portal and non-portal components listening for the same events, event firing order might surprise you.&lt;/p&gt;

&lt;p&gt;Test carefully and consider using native event listeners in tricky cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Should You Reach for Portals?
&lt;/h2&gt;

&lt;p&gt;Use portals when you need UI elements that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Break out of clipping or stacking of parent containers&lt;/li&gt;
&lt;li&gt;Need to be siblings of your root app node for layering&lt;/li&gt;
&lt;li&gt;Require focus management separate from the main React tree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modals, tooltips, dropdown menus, fullscreen overlays, and context menus are classic portal use cases.&lt;/p&gt;

&lt;p&gt;If your popup content can stay within parent containers without clipping or z-index issues, portals might be overkill and add complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Debugging Story
&lt;/h2&gt;

&lt;p&gt;I once built a tooltip that disappeared as soon as I clicked inside it ,  even though I had an outside click handler to close it only if the click was truly outside.&lt;/p&gt;

&lt;p&gt;The problem was the click event inside the portal was bubbling to the portal container, but my handler was checking if the click target was inside the React component tree ,  which it wasn’t.&lt;/p&gt;

&lt;p&gt;Solution: I switched to checking if the click target was inside the portal container DOM node instead. Suddenly, clicks inside the tooltip didn’t trigger the outside click handler.&lt;/p&gt;

&lt;p&gt;That tiny change saved me hours of head-scratching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;React portals let you render components outside your main React root for UI elements like modals and tooltips. This is powerful but comes with quirks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event propagation crosses DOM boundaries differently than standard React tree events.&lt;/li&gt;
&lt;li&gt;Focus management requires explicit handling because the portal’s DOM nodes are outside your main tree.&lt;/li&gt;
&lt;li&gt;Debugging portal issues means thinking in terms of DOM containment and event flow outside React’s usual model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time your modal misbehaves or your tooltip’s keyboard navigation feels off, remember: portals are working exactly as designed ,  but you might need to adapt your event and focus logic to their outside-the-tree reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/react-portals-how-they-work-and-when-to-use-them-for-modals-and-tooltips" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>portals</category>
      <category>modals</category>
      <category>tooltips</category>
    </item>
    <item>
      <title>Debugging React Memoization: When useMemo and React.memo Backfire</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:47:42 +0000</pubDate>
      <link>https://dev.to/mspk97/debugging-react-memoization-when-usememo-and-reactmemo-backfire-4ied</link>
      <guid>https://dev.to/mspk97/debugging-react-memoization-when-usememo-and-reactmemo-backfire-4ied</guid>
      <description>&lt;p&gt;You’ve wrapped a component with &lt;code&gt;React.memo&lt;/code&gt; or used &lt;code&gt;useMemo&lt;/code&gt; to optimize rendering. You expect fewer renders, snappier UI, maybe even a better frame rate.&lt;/p&gt;

&lt;p&gt;But then, surprise! Your component still re-renders way more often than you think it should. Or worse, it seems stuck showing stale props or state.&lt;/p&gt;

&lt;p&gt;This exact problem had me banging my head against the wall recently. Memoization in React isn’t magic ,  it’s a subtle mechanism with traps that can silently backfire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Memoization Exists in React
&lt;/h2&gt;

&lt;p&gt;React components re-render when their parent renders, their props change, or their state updates. Sometimes you want to avoid unnecessary work when inputs haven’t changed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; is a higher-order component that shallowly compares props and skips rendering if they’re equal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; caches a computed value between renders, recomputing only when dependencies change.&lt;/p&gt;

&lt;p&gt;Sounds straightforward, right? But the devil is in how equality is checked and dependencies are tracked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shallow Equality Trap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; does a shallow comparison of props using &lt;code&gt;Object.is&lt;/code&gt; by default. That means it only compares primitive values or references, not deep object contents.&lt;/p&gt;

&lt;p&gt;Here’s a classic example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MyComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rendering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MyComponent&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’d expect &lt;code&gt;MyComponent&lt;/code&gt; to render once, right? Nope. Every render of &lt;code&gt;Parent&lt;/code&gt; creates a fresh &lt;code&gt;user&lt;/code&gt; object with a new reference, so &lt;code&gt;React.memo&lt;/code&gt; sees different props and re-renders.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s going on?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; compares the previous &lt;code&gt;user&lt;/code&gt; prop and the new one by reference. Since &lt;code&gt;Parent&lt;/code&gt; creates a new object inline, &lt;code&gt;React.memo&lt;/code&gt; thinks props changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix it?
&lt;/h2&gt;

&lt;p&gt;Memoize the object outside render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MyComponent&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;MyComponent&lt;/code&gt; skips renders unless &lt;code&gt;user&lt;/code&gt; changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When useMemo Doesn’t Memoize What You Think
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; works similarly ,  it caches a value between renders, recomputing only if dependencies change. But it’s easy to misuse.&lt;/p&gt;

&lt;p&gt;Consider this snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memoizedValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;computeExpensive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;data&lt;/code&gt; is an object or array recreated every render, your memoization fails because the dependency changes every time.&lt;/p&gt;

&lt;p&gt;This is a common pitfall when you see &lt;code&gt;useMemo&lt;/code&gt; not preventing expensive recalculations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debug tip:
&lt;/h2&gt;

&lt;p&gt;Log your dependencies and check if their references are stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stale Props and State Due to Memoization
&lt;/h2&gt;

&lt;p&gt;Another tricky scenario: your memoized component doesn’t update when props seem to change.&lt;/p&gt;

&lt;p&gt;This usually happens because of stale closures or incorrect dependency arrays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MyComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Clicked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Click me&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the parent passes a new &lt;code&gt;onClick&lt;/code&gt; prop every render but your component uses a memoized &lt;code&gt;handleClick&lt;/code&gt; with an empty array, it might ignore the new prop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s the solution?
&lt;/h2&gt;

&lt;p&gt;Make sure callbacks and memoized values depend on all relevant props and state.&lt;/p&gt;

&lt;h2&gt;
  
  
  React.memo with Custom Comparison Functions
&lt;/h2&gt;

&lt;p&gt;If your props are complex objects that change frequently but contain stable values, you can provide a custom comparator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;areEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prevProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;nextProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MyComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;areEqual&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you control when to skip renders more precisely.&lt;/p&gt;

&lt;p&gt;But beware: writing incorrect comparators can cause subtle bugs where your UI doesn’t update as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Memoization Issues Step-by-Step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Log Every Render&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add &lt;code&gt;console.log&lt;/code&gt; inside your component to see when it renders.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check Prop References&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Log props before passing them to memoized components. Use &lt;code&gt;console.log(prop, Object.is(prevProp, prop))&lt;/code&gt; to check if references change.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Inspect Dependency Arrays&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For &lt;code&gt;useMemo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt;, ensure dependencies include everything used inside the function.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use React DevTools Profiler&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It highlights which components re-render and why. You can spot unexpected renders this way.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Try Removing Memoization&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Temporarily remove &lt;code&gt;React.memo&lt;/code&gt; or &lt;code&gt;useMemo&lt;/code&gt; to see if behavior changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use Memoization
&lt;/h2&gt;

&lt;p&gt;Memoization isn’t free. It adds complexity and some overhead.&lt;/p&gt;

&lt;p&gt;If your components are cheap to render or your app isn’t bottlenecked by rendering, memoization might be premature optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;React’s memoization tools are powerful but delicate. They rely on reference equality and correct dependencies. If you don’t get those right, you get surprise re-renders or stale UI.&lt;/p&gt;

&lt;p&gt;The key is to treat memoization as a contract ,  you must keep references stable and dependencies accurate. When that contract breaks, React’s memoization works against you.&lt;/p&gt;

&lt;p&gt;Next time your &lt;code&gt;useMemo&lt;/code&gt; or &lt;code&gt;React.memo&lt;/code&gt; seems broken, grab your console, check those references, and track down the sneaky object or function that’s tripping you up.&lt;/p&gt;

&lt;p&gt;Happy debugging!&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/" rel="noopener noreferrer"&gt;web.dev performance guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React documentation&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/debugging-react-memoization-when-usememo-and-react-memo-backfire" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>memoization</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Keyboard Accessibility in Custom Components: Implementing and Debugging Focus Management</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Wed, 12 Aug 2026 10:38:47 +0000</pubDate>
      <link>https://dev.to/mspk97/keyboard-accessibility-in-custom-components-implementing-and-debugging-focus-management-23j</link>
      <guid>https://dev.to/mspk97/keyboard-accessibility-in-custom-components-implementing-and-debugging-focus-management-23j</guid>
      <description>&lt;p&gt;Ever tried tabbing through a custom dropdown or a fancy toggle switch you built, only to find the keyboard focus just skips over it, or worse, gets trapped inside with no way out? I’ve been there, frustrated by a UI that looked perfect but was a nightmare to navigate without a mouse.&lt;/p&gt;

&lt;p&gt;Keyboard accessibility can easily slip through the cracks when you roll your own components. Native elements like &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; come with built-in keyboard behavior and focus handling baked in. Custom stuff? Not so much. You get the visuals, but the focus order, keyboard events, and screen reader cues? That’s all on you.&lt;/p&gt;

&lt;p&gt;Let me walk you through the key challenges I ran into, how keyboard focus really works under the hood, and some concrete ways to implement and debug accessible keyboard navigation in custom UI components.&lt;/p&gt;

&lt;h2&gt;
  
  
  The focus trap: Why keyboard users get stuck or skipped
&lt;/h2&gt;

&lt;p&gt;I once built a custom dropdown with divs and spans for styling freedom. It looked slick, but when I tested keyboard navigation, tabbing would jump right past it. Or sometimes the focus would enter the dropdown, but arrow keys didn’t move selection as expected. Worse, shift+tabbing out of the dropdown was impossible; the focus got stuck inside.&lt;/p&gt;

&lt;p&gt;Why? Because by default, only certain elements are &lt;em&gt;focusable&lt;/em&gt; ,  mostly interactive native elements like buttons and inputs. A div or span? Not focusable unless you add &lt;code&gt;tabindex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had neglected to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make the dropdown toggle button focusable with &lt;code&gt;tabindex="0"&lt;/code&gt; (or better, use a native &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Manage keyboard events like ArrowUp, ArrowDown, Escape to open/close and navigate items&lt;/li&gt;
&lt;li&gt;Control focus movement on open to the first menu item&lt;/li&gt;
&lt;li&gt;Restore focus to the toggle when closing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without explicit focus management, keyboard users either can’t reach the dropdown or get lost inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How browser focus and tab order really work
&lt;/h2&gt;

&lt;p&gt;Browsers assign a &lt;em&gt;tab order&lt;/em&gt; based on the document’s focusable elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native focusable elements (&lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;, etc.) come first&lt;/li&gt;
&lt;li&gt;Elements with positive &lt;code&gt;tabindex&lt;/code&gt; (rarely recommended)&lt;/li&gt;
&lt;li&gt;Elements with &lt;code&gt;tabindex=0&lt;/code&gt; (focusable in DOM order)&lt;/li&gt;
&lt;li&gt;Elements with negative &lt;code&gt;tabindex&lt;/code&gt; are focusable programmatically but skipped in tab order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your custom component’s parts lack &lt;code&gt;tabindex=0&lt;/code&gt; or aren’t native focusables, the keyboard will skip them.&lt;/p&gt;

&lt;p&gt;Also, when you open something like a menu, you typically want to move focus into it programmatically (e.g., focus the first menu item). This prevents keyboard users from losing context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing keyboard support in custom components
&lt;/h2&gt;

&lt;p&gt;Here’s a checklist I use when coding custom keyboard interactions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use semantic elements whenever possible.&lt;/strong&gt; If your toggle behaves like a button, use &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make interactive elements focusable.&lt;/strong&gt; Use &lt;code&gt;tabindex="0"&lt;/code&gt; on divs or spans if needed, but sparingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle keyboard events explicitly.&lt;/strong&gt; Listen for &lt;code&gt;keydown&lt;/code&gt; events on your component, and respond to keys like Enter, Space, Arrow keys, Escape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manage focus on open/close.&lt;/strong&gt; When a dropdown opens, move focus to the first item. When it closes, return focus to the toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trap focus if needed.&lt;/strong&gt; For modal dialogs or complex widgets, keep focus inside until the user closes or presses Escape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use ARIA roles and attributes properly.&lt;/strong&gt; &lt;code&gt;role="menu"&lt;/code&gt;, &lt;code&gt;role="menuitem"&lt;/code&gt;, &lt;code&gt;aria-expanded&lt;/code&gt;, &lt;code&gt;aria-haspopup&lt;/code&gt;, and &lt;code&gt;aria-activedescendant&lt;/code&gt; help assistive tech understand your widget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, for a custom dropdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
  &lt;span class="na"&gt;aria-haspopup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"listbox"&lt;/span&gt;
  &lt;span class="na"&gt;aria-expanded&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleOpen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Select an option
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"listbox"&lt;/span&gt;
    &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;listboxRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;onKeyDown&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleKeyDown&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"option"&lt;/span&gt;
        &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;focusedIndex&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;selectOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onFocus&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setFocusedIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;handleKeyDown&lt;/code&gt;, you’d manage ArrowUp/Down to move &lt;code&gt;focusedIndex&lt;/code&gt;, Enter or Space to select, and Escape to close and return focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging keyboard navigation woes
&lt;/h2&gt;

&lt;p&gt;Testing keyboard accessibility is simple but powerful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Tab and Shift+Tab to navigate through your app’s interactive elements.&lt;/li&gt;
&lt;li&gt;Use screen reader tools to check announcements and focus highlights.&lt;/li&gt;
&lt;li&gt;Inspect the Accessibility Tree with browser devtools to verify roles and focusability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If tab skips your component, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the toggle have a native focusable element or &lt;code&gt;tabindex="0"&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Are interactive items focusable and in tab order?&lt;/li&gt;
&lt;li&gt;Are you accidentally using &lt;code&gt;tabindex="-1"&lt;/code&gt; where you want tab to land?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If focus gets trapped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are you managing focus programmatically on open and close?&lt;/li&gt;
&lt;li&gt;Do you have a focus trap or modal code that might be overzealous?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If keyboard events don’t work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are you listening for &lt;code&gt;keydown&lt;/code&gt; instead of &lt;code&gt;keypress&lt;/code&gt; or &lt;code&gt;keyup&lt;/code&gt;? &lt;code&gt;keydown&lt;/code&gt; is best for handling navigation keys.&lt;/li&gt;
&lt;li&gt;Are you calling &lt;code&gt;event.preventDefault()&lt;/code&gt; appropriately to avoid default scrolling or key behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser devtools Accessibility pane lets you simulate keyboard focus and see the accessibility tree. Also, extensions like Axe or Lighthouse can flag common accessibility issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond keyboard: Why accessibility is a team sport
&lt;/h2&gt;

&lt;p&gt;Focus management is just one piece of accessibility. ARIA roles, visual focus indicators, announcements, and semantic HTML all matter. When you build custom UI, you’re responsible for recreating these affordances.&lt;/p&gt;

&lt;p&gt;I learned that the best practice is to start with semantic HTML and enhance, rather than replace, native behavior. Use custom components only when necessary, and always test early and often with keyboard and assistive tech.&lt;/p&gt;

&lt;p&gt;If you’re stuck, look at well-tested open source libraries like Reach UI or Radix UI. Their source code is a goldmine for how to handle focus and keyboard events right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Keyboard accessibility is not a checkbox ,  it’s a mindset. When you build a custom UI, imagine your app without a mouse. Walk through it with Tab. If you hit a blind spot, that’s where focus management needs work.&lt;/p&gt;

&lt;p&gt;Fixing keyboard navigation bugs might feel tedious, but it makes your app usable by millions who rely on keyboards or assistive technology. And often, the fixes improve your code quality and UX for everyone.&lt;/p&gt;

&lt;p&gt;So next time you craft a custom dropdown, toggle, or modal, don’t just make it look good ,  make sure you can &lt;em&gt;tab&lt;/em&gt; through it cleanly, see where you are, and never get stuck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful learning resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;W3C WAI accessibility guidance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/company/w3schools-com" rel="noopener noreferrer"&gt;W3Schools accessibility tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/keyboard-accessibility-in-custom-components-implementing-and-debugging-focus-management" rel="noopener noreferrer"&gt;Under The Hood&lt;/a&gt;.
Get the next deep dive in your inbox: &lt;a href="https://blog.mspk.me/#subscribe" rel="noopener noreferrer"&gt;subscribe to Under The Hood&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>keyboard</category>
      <category>focusmanagement</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
