<?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>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>
    <item>
      <title>Browser Garbage Collection: What Frontend Engineers Should Know</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Mon, 10 Aug 2026 10:41:40 +0000</pubDate>
      <link>https://dev.to/mspk97/browser-garbage-collection-what-frontend-engineers-should-know-3kdp</link>
      <guid>https://dev.to/mspk97/browser-garbage-collection-what-frontend-engineers-should-know-3kdp</guid>
      <description>&lt;p&gt;Ever had your web app slow down after a while or start feeling like it’s chewing through more and more memory? You might have suspected memory leaks ,  but where exactly do they come from in a browser? How does the browser even decide what memory to free or keep? &lt;/p&gt;

&lt;p&gt;I hit this problem recently debugging a single-page app that got sluggish after a few minutes. Some tabs just ballooned their memory usage, and Chrome’s Task Manager was confirming my suspicion: memory just kept climbing.&lt;/p&gt;

&lt;p&gt;Turns out, understanding how browsers do garbage collection under the hood cleared up a lot of mystery. It’s not just about your JavaScript variables. The DOM, event listeners, and even closures can silently hold onto memory long after you think you’re done with them.&lt;/p&gt;

&lt;p&gt;Let’s walk through what’s really going on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Garbage collection basics: the browser’s janitor
&lt;/h2&gt;

&lt;p&gt;Your browser’s JavaScript engine, like V8 in Chrome or SpiderMonkey in Firefox, runs your scripts and manages memory. Garbage collection (GC) is the process that finds objects in memory that are no longer reachable by your running code and frees that memory so the app doesn’t grow endlessly.&lt;/p&gt;

&lt;p&gt;But how does the browser know what’s "no longer reachable"? It uses a concept called &lt;strong&gt;reachability&lt;/strong&gt;. If your code or the system can still access a value through some chain of references, it’s considered live. Anything else is garbage and eligible to be collected.&lt;/p&gt;

&lt;p&gt;Imagine a graph where nodes are objects and edges are references. GC algorithms like &lt;strong&gt;mark-and-sweep&lt;/strong&gt; start from root objects (globals, stack variables) and mark everything reachable. Then everything unmarked is swept away.&lt;/p&gt;

&lt;p&gt;But things get tricky when you add the DOM and event listeners into the mix.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript objects and closures: Your typical GC suspects
&lt;/h2&gt;

&lt;p&gt;When you create variables, objects, functions, or closures, they live in memory as long as you keep references to them. For 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;makeCounter&lt;/span&gt;&lt;span class="p"&gt;()&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;count&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="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="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;count&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;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeCounter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the closure returned by &lt;code&gt;makeCounter&lt;/code&gt; keeps a hidden reference to &lt;code&gt;count&lt;/code&gt;. Garbage collection won’t free &lt;code&gt;count&lt;/code&gt; as long as the &lt;code&gt;counter&lt;/code&gt; function exists.&lt;/p&gt;

&lt;p&gt;This is straightforward, but leaks happen when you accidentally keep references alive longer than needed. For example, storing large objects in global variables or arrays you never clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DOM’s role in memory management
&lt;/h2&gt;

&lt;p&gt;Unlike plain JavaScript objects, DOM nodes are managed both by the browser’s rendering engine and the JavaScript engine. When you create elements:&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;div&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;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser creates a DOM tree node for that element. But what happens when you remove it?&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might &lt;em&gt;think&lt;/em&gt; the node is gone, but if you still hold a JavaScript reference to &lt;code&gt;div&lt;/code&gt;, the browser cannot reclaim that memory. That node ,  and all its children and event listeners ,  remain in memory.&lt;/p&gt;

&lt;p&gt;This is a classic source of leaks: detached DOM nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event listeners: The sneaky retainers
&lt;/h2&gt;

&lt;p&gt;Event listeners can silently keep DOM nodes and other objects alive. Consider this:&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;button&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;button&lt;/span&gt;&lt;span class="dl"&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;onClick&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;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="nx"&gt;button&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;click&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// Later&lt;/span&gt;
&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you remove &lt;code&gt;button&lt;/code&gt; from the DOM but never call &lt;code&gt;button.removeEventListener('click', onClick)&lt;/code&gt;, the event listener reference stays registered. Many browsers keep that listener and the node alive because the event system holds a reference to the callback and the node.&lt;/p&gt;

&lt;p&gt;This leads to &lt;strong&gt;memory leaks from unremoved event listeners&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common memory leak patterns to watch out for
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Detached DOM nodes:&lt;/strong&gt; You remove elements from the document but keep references to them in JavaScript variables or closures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Forgotten event listeners:&lt;/strong&gt; Adding listeners without removing them when elements or components unmount.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Closures over large objects:&lt;/strong&gt; Closures accidentally capturing large data structures that are no longer needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timers and intervals:&lt;/strong&gt; Using &lt;code&gt;setInterval&lt;/code&gt; or &lt;code&gt;setTimeout&lt;/code&gt; without clearing them when components unmount, keeping references alive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Caches that grow indefinitely:&lt;/strong&gt; Storing data in arrays or maps without eviction policies.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How browsers perform garbage collection under the hood
&lt;/h2&gt;

&lt;p&gt;Modern JavaScript engines use generational garbage collection. They optimize for the fact that most objects die young.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Young generation:&lt;/strong&gt; Newly created objects are allocated here. GC runs frequently and quickly here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old generation:&lt;/strong&gt; Objects that survive multiple GC cycles are promoted here. GC runs less frequently but is more thorough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means your memory leaks often occur in the old generation ,  objects that never get freed because something still holds a reference.&lt;/p&gt;

&lt;p&gt;When a GC cycle runs, it starts from root objects like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global window and document objects&lt;/li&gt;
&lt;li&gt;Active function call stacks&lt;/li&gt;
&lt;li&gt;Registered event listeners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and marks all reachable objects.&lt;/p&gt;

&lt;p&gt;Anything not reachable is cleaned up, including detached DOM nodes and unused JavaScript objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging memory leaks: Practical tips
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Chrome DevTools Memory tab
&lt;/h2&gt;

&lt;p&gt;Open Chrome DevTools and go to the &lt;strong&gt;Memory&lt;/strong&gt; panel.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heap snapshot:&lt;/strong&gt; Take snapshots before and after interactions to see what objects remain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocation instrumentation:&lt;/strong&gt; Record allocations over time to find growing memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocation sampling:&lt;/strong&gt; Profile allocations to find high-memory objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Detect detached DOM nodes
&lt;/h2&gt;

&lt;p&gt;In the &lt;strong&gt;Elements&lt;/strong&gt; panel, you can search for nodes that are no longer attached to the document but still in memory.&lt;/p&gt;

&lt;p&gt;You can also use this snippet to find detached nodes:&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;all&lt;/span&gt; &lt;span class="o"&gt;=&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;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;*&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;detached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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="nx"&gt;detached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Chrome DevTools also lets you inspect event listeners on DOM nodes. Make sure listeners are removed when not needed.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;getEventListeners(node)&lt;/code&gt; in the console to list them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heap snapshot comparisons
&lt;/h2&gt;

&lt;p&gt;Take snapshots at different times and compare to see what objects persist unexpectedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiling closures
&lt;/h2&gt;

&lt;p&gt;Closures holding memory can be tricky. Look for retained objects in heap snapshots and trace retainers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical habits to avoid leaks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always clean up event listeners when elements or components unmount.&lt;/li&gt;
&lt;li&gt;Remove references to DOM nodes after removing them from the document.&lt;/li&gt;
&lt;li&gt;Clear timers and intervals when no longer needed.&lt;/li&gt;
&lt;li&gt;Avoid storing large data in global variables or long-lived caches without limits.&lt;/li&gt;
&lt;li&gt;Use tools like Chrome DevTools regularly to profile memory, especially after complex UI interactions or dynamic content loads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When frameworks help ,  and when they don’t
&lt;/h2&gt;

&lt;p&gt;Modern frontend frameworks like React, Vue, and Angular help manage DOM lifecycle and event listeners for you. But leaks still happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you create event listeners outside the framework lifecycle.&lt;/li&gt;
&lt;li&gt;If you keep references to DOM nodes in variables or closures.&lt;/li&gt;
&lt;li&gt;If you misuse refs or fail to clean up timers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding browser GC mechanics helps you spot these issues even when using frameworks.&lt;/p&gt;




&lt;p&gt;Next time your web app feels sluggish or your tab’s memory ballooning, remember it’s usually a matter of references the browser can’t drop. The garbage collector is silently cleaning up what it can, but it can’t free what you still hold.&lt;/p&gt;

&lt;p&gt;Knowing how JavaScript objects, DOM nodes, and event listeners interact with memory lets you write cleaner code, prevent leaks, and keep your app running smoothly ,  no magic, just a bit of detective work under the hood.&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/browser-garbage-collection-what-frontend-engineers-should-know" 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>browser</category>
      <category>javascript</category>
      <category>memory</category>
      <category>garbagecollection</category>
    </item>
    <item>
      <title>React Suspense and Error Boundaries: How They Interact and How to Debug Failures</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Thu, 06 Aug 2026 14:02:57 +0000</pubDate>
      <link>https://dev.to/mspk97/react-suspense-and-error-boundaries-how-they-interact-and-how-to-debug-failures-4i1e</link>
      <guid>https://dev.to/mspk97/react-suspense-and-error-boundaries-how-they-interact-and-how-to-debug-failures-4i1e</guid>
      <description>&lt;p&gt;Ever had your React component throw an error inside a Suspense boundary and wondered why neither your Suspense fallback nor your Error Boundary’s fallback UI showed up?&lt;/p&gt;

&lt;p&gt;I ran into this exact headache recently. I wrapped my lazy-loaded component with Suspense and an Error Boundary, expecting either a spinner or an error message. Instead, I got… nothing. Blank screen. No clues.&lt;/p&gt;

&lt;p&gt;Turns out, Suspense and Error Boundaries have a subtle but important dance happening under the hood. Let me walk you through what’s really going on, why React sometimes seems to silently swallow errors or fallbacks, and how you can debug these tricky states.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment when Suspense and errors collide
&lt;/h2&gt;

&lt;p&gt;Imagine this setup:&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="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&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="nc"&gt;LoadingSpinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt; &lt;span class="na"&gt;fallback&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="nc"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LazyComponent&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;LazyComponent&lt;/code&gt; suspends while fetching data, so the spinner shows up. Great. But what if &lt;code&gt;LazyComponent&lt;/code&gt; throws an error before the data arrives? You’d expect the &lt;code&gt;ErrorBoundary&lt;/code&gt; to catch that and show the error message, right?&lt;/p&gt;

&lt;p&gt;Not always.&lt;/p&gt;

&lt;p&gt;In React’s commit phase ,  that is, when React applies changes to the DOM ,  if a component suspends, React bails out of rendering that tree and instead shows the Suspense fallback immediately. But if the same component throws an error (instead of suspending), React tries to find the nearest Error Boundary to handle it.&lt;/p&gt;

&lt;p&gt;Here’s the catch: Suspense boundaries only catch suspensions. They don’t catch errors. Error Boundaries catch errors but don’t catch suspensions.&lt;/p&gt;

&lt;p&gt;When a component inside Suspense throws an error, React’s update flow attempts to recover by unwinding to the Error Boundary. But if Suspense is the outer wrapper, React’s internal scheduling and commit phases can get tangled, causing neither fallback UI to appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  What React does internally when errors happen inside Suspense
&lt;/h2&gt;

&lt;p&gt;React uses a concept called "lanes" to prioritize updates and tracks whether a component tree is suspended or errored during render and commit phases.&lt;/p&gt;

&lt;p&gt;When an error is thrown during rendering, React marks the fiber tree as "errored" and looks up the tree for the closest Error Boundary to handle it. If found, React schedules an error recovery update to replace the errored subtree with the Error Boundary’s fallback UI.&lt;/p&gt;

&lt;p&gt;But if that errored subtree is inside a Suspense boundary that also suspended, React’s internal state can get confused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Suspense boundaries expect a suspension to show fallback UI.&lt;/li&gt;
&lt;li&gt;Error Boundaries expect an error to show fallback UI.&lt;/li&gt;
&lt;li&gt;When both happen close together or in nested trees, React prioritizes suspensions and sometimes skips error handling in that render cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can cause your app to render nothing or show stale UI with no clear indication of what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common bugs that cause fallback UI to silently fail
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Wrapping Suspense &lt;em&gt;outside&lt;/em&gt; Error Boundaries
&lt;/h2&gt;

&lt;p&gt;In the example earlier, Suspense wraps the Error Boundary. This means Suspense tries to handle suspensions first. If an error happens inside, Suspense doesn’t catch it ,  and the error bubbles up past Suspense.&lt;/p&gt;

&lt;p&gt;But React’s commit phase handles Suspense boundaries differently than Error Boundaries, and because Suspense is outermost, React can get stuck showing no fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Wrap Suspense &lt;em&gt;inside&lt;/em&gt; the Error Boundary instead:&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="nc"&gt;ErrorBoundary&lt;/span&gt; &lt;span class="na"&gt;fallback&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="nc"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;/&amp;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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&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="nc"&gt;LoadingSpinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LazyComponent&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now errors get caught by the Error Boundary before Suspense tries to handle suspensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Throwing errors asynchronously after suspending
&lt;/h2&gt;

&lt;p&gt;If your component suspends by throwing a Promise, then later throws an error during commit or an effect, React’s update flow can get tricky. The Suspense fallback might show briefly, then the error triggers. But the Error Boundary’s fallback may not replace the Suspense fallback immediately, causing flickers or blank UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Nested Suspense boundaries with missing Error Boundaries
&lt;/h2&gt;

&lt;p&gt;If you have multiple nested Suspense boundaries but no Error Boundary catching errors inside, React renders fallback UIs for suspensions but crashes silently on errors, often logging cryptic warnings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging strategies to untangle Suspense + Error Boundary issues
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Inspect React DevTools Fiber tree
&lt;/h2&gt;

&lt;p&gt;React DevTools now show Suspense and Error Boundary components explicitly. When your UI disappears or fallback UI doesn’t appear, inspect the fiber tree.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Suspense boundaries marked as "suspended"&lt;/li&gt;
&lt;li&gt;Error Boundaries marked as "errored"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you see Suspense suspended but no error, it means React is still waiting on a Promise.&lt;/p&gt;

&lt;p&gt;If you see Error Boundaries errored but no fallback UI, React might be skipping commit phases or your fallback component itself is throwing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add logging in error handlers
&lt;/h2&gt;

&lt;p&gt;In your Error Boundary’s &lt;code&gt;componentDidCatch&lt;/code&gt; or &lt;code&gt;getDerivedStateFromError&lt;/code&gt;, log the error and info objects. This confirms the boundary is catching the error.&lt;/p&gt;

&lt;p&gt;If you never see logs, React didn’t find your boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use &lt;code&gt;React.unstable_DebugTracingMode&lt;/code&gt; (experimental)
&lt;/h2&gt;

&lt;p&gt;In recent React versions, enabling this mode helps trace Suspense and error boundary interactions in the console, showing when boundaries suspend, resume, or error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your error fallback components
&lt;/h2&gt;

&lt;p&gt;Sometimes Error Boundary fallbacks throw errors themselves, causing React to unmount everything.&lt;/p&gt;

&lt;p&gt;Wrap fallback UI in try/catch or keep it simple to isolate this.&lt;/p&gt;

&lt;h2&gt;
  
  
  How React’s update phases influence fallback rendering
&lt;/h2&gt;

&lt;p&gt;React separates rendering into two steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Render phase:&lt;/strong&gt; React calls your components to build a new fiber tree. This phase is pure ,  no side effects or DOM mutations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit phase:&lt;/strong&gt; React applies changes to the DOM, runs effects, and handles errors thrown here differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suspensions happen during render phase, triggering React to delay committing the new UI and show Suspense fallback instead.&lt;/p&gt;

&lt;p&gt;Errors can happen during render or commit.&lt;/p&gt;

&lt;p&gt;If an error occurs during render, React tries to recover by rendering the nearest Error Boundary’s fallback in the next render phase.&lt;/p&gt;

&lt;p&gt;If an error happens during commit (e.g., in an effect or lifecycle method), React unmounts the whole tree unless you have an error boundary.&lt;/p&gt;

&lt;p&gt;Understanding this helps you see why sometimes the Suspense fallback shows, but the Error Boundary never does ,  or vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical takeaway: how to organize Suspense and Error Boundaries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Wrap Suspense inside Error Boundary if you want to catch errors thrown inside lazy or suspenseful components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep your error fallback UI simple and robust to avoid cascading failures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid throwing errors asynchronously after suspensions if possible, or handle those with additional boundaries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test your boundaries by manually throwing errors and suspending promises to see the fallback behavior.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;React’s Suspense and Error Boundaries are powerful but can trip you up when they interact. When your fallback UI disappears or you see a blank screen, the problem is often in how these boundaries are nested and how React schedules updates.&lt;/p&gt;

&lt;p&gt;With a little digging into React’s phases and careful boundary placement, you can tame these quirks and build reliable loading and error states. Next time you hit a ghostly blank screen, you’ll know exactly where to look.&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/react-suspense-and-error-boundaries-how-they-interact-and-how-to-debug-failures" 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>suspense</category>
      <category>errorboundaries</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Debugging Complex Pointer and Touch Event Interactions in Mobile Browsers</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Tue, 04 Aug 2026 14:31:01 +0000</pubDate>
      <link>https://dev.to/mspk97/debugging-complex-pointer-and-touch-event-interactions-in-mobile-browsers-393f</link>
      <guid>https://dev.to/mspk97/debugging-complex-pointer-and-touch-event-interactions-in-mobile-browsers-393f</guid>
      <description>&lt;p&gt;Ever tried to build a smooth swipe or pinch gesture on mobile only to find it firing weird events, missing touches, or blocking scroll? If you’ve been there, you know it quickly devolves into a maze of event handlers, mysterious no-ops, and contradictory docs. &lt;/p&gt;

&lt;p&gt;I recently spent days debugging a custom gesture recognizer for a mobile web app and finally got to the bottom of how browsers handle pointer, touch, and gesture events differently on mobile versus desktop. Spoiler: it’s not just about firing different events ,  it’s the event order, default behaviors, and subtle browser quirks that trip you up.&lt;/p&gt;

&lt;p&gt;Let’s get practical. I’ll share concrete examples, what’s going on under the hood, and exactly how to debug these interactions using Chrome DevTools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment things got weird: touch events firing but pointer events missing
&lt;/h2&gt;

&lt;p&gt;My app needed to detect a two-finger swipe. I started by listening to &lt;code&gt;pointerdown&lt;/code&gt;, &lt;code&gt;pointermove&lt;/code&gt;, and &lt;code&gt;pointerup&lt;/code&gt; events because pointer events unify mouse, touch, and pen input nicely on desktop. &lt;/p&gt;

&lt;p&gt;On laptop browsers, this was smooth sailing. But on mobile, sometimes the pointer events never fired, or fired inconsistently. I was baffled.&lt;/p&gt;

&lt;p&gt;Digging deeper, I learned that on mobile browsers, pointer events are often built on top of touch events but with extra layers of logic to handle gestures and scrolling. Sometimes the browser suppresses pointer events after detecting a gesture like scrolling, or delays them until it’s sure the user isn’t zooming. This behavior differs a lot across browsers and platforms.&lt;/p&gt;

&lt;p&gt;Here’s a quick 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="nx"&gt;el&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;touchstart&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;touchstart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;el&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;pointerdown&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;pointerdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On desktop emulation, you’ll see both events fire. But on real mobile Safari, &lt;code&gt;pointerdown&lt;/code&gt; might not fire at all if the browser thinks the touch might become a gesture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why event sequences differ so much on mobile
&lt;/h2&gt;

&lt;p&gt;The event pipeline on mobile is more complex because the browser must decide whether a touch is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tap&lt;/li&gt;
&lt;li&gt;A scroll&lt;/li&gt;
&lt;li&gt;A zoom (pinch or double tap)&lt;/li&gt;
&lt;li&gt;A custom gesture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To do this, browsers delay or cancel pointer events based on heuristics like touch slop (how far a finger moves before the gesture is considered a scroll).&lt;/p&gt;

&lt;p&gt;For example, if the user places a finger and starts scrolling, the browser may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fire &lt;code&gt;touchstart&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Delay or cancel &lt;code&gt;pointerdown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fire &lt;code&gt;touchmove&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cancel pointer events entirely if scrolling starts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means your pointer event handlers might never get called if the browser claims the gesture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tricky default behaviors and event.preventDefault()
&lt;/h2&gt;

&lt;p&gt;Another common trap is the interaction between the browser’s default touch behaviors (like scrolling and zooming) and your event handlers.&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;event.preventDefault()&lt;/code&gt; on &lt;code&gt;touchstart&lt;/code&gt; or &lt;code&gt;touchmove&lt;/code&gt; can stop scrolling, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On some browsers, &lt;code&gt;touch-action&lt;/code&gt; CSS is a better way to declare which gestures your app handles&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;preventDefault()&lt;/code&gt; indiscriminately can hurt scroll performance and cause jank&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, to enable custom horizontal swipe but preserve vertical scroll, setting CSS like this helps:&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;.swipe-area&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;touch-action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pan-y&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 tells the browser: "I want to handle horizontal gestures myself, but let vertical scrolling happen normally." Browsers then won’t cancel pointer events for vertical scrolls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls with custom gesture recognizers
&lt;/h2&gt;

&lt;p&gt;If you’re building gestures from scratch, here are things I ran into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mixing touch and pointer events without clear strategy:&lt;/strong&gt; Listening to both can cause duplicate events or missed ones if you don’t account for the browser’s gesture detection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not using &lt;code&gt;touch-action&lt;/code&gt; properly:&lt;/strong&gt; Without it, browsers may cancel pointer events or delay them, breaking your recognizer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ignoring multiple pointers:&lt;/strong&gt; Mobile touch means multiple fingers at once. Pointer events help here, but on some browsers, pointer events don’t fire or lose track of pointer IDs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Relying on &lt;code&gt;event.preventDefault()&lt;/code&gt; too much:&lt;/strong&gt; Blocks scrolling and hurts performance.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I debugged these issues in DevTools
&lt;/h2&gt;

&lt;p&gt;Mobile event debugging is tricky because you need to see event timing and ordering on real devices. Here’s what helped me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remote debugging with Chrome DevTools:&lt;/strong&gt; Connect your Android device via USB and use &lt;code&gt;chrome://inspect&lt;/code&gt; to debug mobile Chrome. You can set breakpoints in event handlers and watch event objects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logging event sequences:&lt;/strong&gt; I added console logs for every pointer, touch, and gesture event with timestamps and pointer IDs, so I could see exactly which events fired and in what order.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using the "Event Listener Breakpoints" feature:&lt;/strong&gt; In DevTools, under Sources &amp;gt; Event Listener Breakpoints, I enabled breakpoints for &lt;code&gt;touch&lt;/code&gt; and &lt;code&gt;pointer&lt;/code&gt; events to pause exactly when they fire.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inspecting CSS touch-action:&lt;/strong&gt; I repeatedly inspected the element’s computed styles to verify &lt;code&gt;touch-action&lt;/code&gt; was set as intended.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testing on multiple browsers:&lt;/strong&gt; I tested on Chrome, Firefox, and Safari on iOS because they behave differently. Sometimes a solution works in one but fails in another.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A concrete example: building a horizontal swipe recognizer that doesn’t block vertical scroll
&lt;/h2&gt;

&lt;p&gt;Here’s a minimal setup that worked after much trial and error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"swipe"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"touch-action: pan-y; width: 100vw; height: 200px; background: #eee;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Swipe me horizontally
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;swipe&lt;/span&gt;&lt;span class="dl"&gt;'&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;startX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;startY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;el&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;pointerdown&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="nx"&gt;startX&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;clientX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;startY&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;clientY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;el&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;pointermove&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;startX&lt;/span&gt; &lt;span class="o"&gt;===&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&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;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startX&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;dy&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;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startY&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;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Horizontal swipe detected&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="c1"&gt;// prevent scrolling horizontally&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;Horizontal swipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dx&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;el&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;pointerup&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;startX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;startY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;touch-action: pan-y&lt;/code&gt; lets vertical scroll happen normally.&lt;/li&gt;
&lt;li&gt;We listen only to pointer events for clarity.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;e.preventDefault()&lt;/code&gt; is called only when a horizontal swipe is actually detected, minimizing interference with scroll.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Mobile pointer and touch event handling feels like a black box until you see the browser’s hesitation and decision tree in action. Knowing the event sequence differences, the role of &lt;code&gt;touch-action&lt;/code&gt;, and how default behaviors affect your handlers can save hours of debugging.&lt;/p&gt;

&lt;p&gt;Next time your custom gesture is flaky on mobile, try logging every touch and pointer event with timestamps, check your &lt;code&gt;touch-action&lt;/code&gt; CSS, and don’t throw &lt;code&gt;preventDefault()&lt;/code&gt; around blindly. Use remote debugging tools to watch the event flow live.&lt;/p&gt;

&lt;p&gt;You’ll find the browser is trying to help you ,  it just wants to make sure scrolling and zooming work smoothly, even while you capture your fancy new gesture.&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://platform.openai.com/docs" rel="noopener noreferrer"&gt;OpenAI developer resources&lt;/a&gt;
Originally published at &lt;a href="https://blog.mspk.me/posts/debugging-complex-pointer-and-touch-event-interactions-in-mobile-browsers" 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>browser</category>
      <category>mobile</category>
      <category>pointerevents</category>
      <category>touchevents</category>
    </item>
    <item>
      <title>React Concurrent Rendering: Scheduling, Interruptions, and Debugging Suspense Boundaries</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:53:45 +0000</pubDate>
      <link>https://dev.to/mspk97/react-concurrent-rendering-scheduling-interruptions-and-debugging-suspense-boundaries-2899</link>
      <guid>https://dev.to/mspk97/react-concurrent-rendering-scheduling-interruptions-and-debugging-suspense-boundaries-2899</guid>
      <description>&lt;p&gt;You know that moment when your React Suspense fallback jumps on the screen, then disappears, then reappears, leaving you wondering if you did something wrong? I’ve been there ,  seeing flickers, multiple loading spinners, or even UI glitches around Suspense felt like chasing ghosts. &lt;/p&gt;

&lt;p&gt;Turns out, React’s concurrent rendering scheduler is doing a lot behind the scenes ,  juggling priorities, pausing work, and restarting it ,  and Suspense boundaries are right in the middle of this dance. Understanding how React schedules work and handles interruptions can save you hours of frustration.&lt;/p&gt;

&lt;h2&gt;
  
  
  React’s concurrent rendering scheduler: what’s it really doing?
&lt;/h2&gt;

&lt;p&gt;React’s concurrent mode isn’t just a fancy name; it means React doesn’t blindly render your entire component tree all at once. Instead, it breaks rendering work into chunks and spreads it out over multiple frames. This keeps your app responsive to user input and other high-priority tasks.&lt;/p&gt;

&lt;p&gt;Imagine you’re painting a huge mural. Instead of finishing it in one go (blocking everything else), you paint a little, step back, listen if someone calls you, then paint some more. React’s scheduler works similarly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Units of work&lt;/strong&gt;: React slices rendering into small units it can pause and resume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priorities&lt;/strong&gt;: Some updates are more urgent ,  like responding to a click ,  so they jump ahead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interruptions&lt;/strong&gt;: If something more important comes up, React pauses current work and switches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This model makes React apps feel snappy even when doing heavy rendering or fetching data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when Suspense enters the scene?
&lt;/h2&gt;

&lt;p&gt;Suspense boundaries are React’s way to say, “Hey, if this component isn’t ready yet (because it’s waiting on data, code, or something else), show this fallback for now.”&lt;/p&gt;

&lt;p&gt;Under the hood, when a component suspends (throws a Promise), React marks that unit of work as "waiting," and the Suspense boundary kicks in to show the fallback UI immediately.&lt;/p&gt;

&lt;p&gt;But here’s the catch: React keeps trying to finish rendering the suspended component in the background. When the Promise resolves, React attempts to commit the real UI.&lt;/p&gt;

&lt;p&gt;Since this happens with React’s concurrent scheduler, the work can be interrupted or retried multiple times before committing, which explains those flickers or fallback flashes you see.&lt;/p&gt;

&lt;h2&gt;
  
  
  How React prioritizes and interrupts updates
&lt;/h2&gt;

&lt;p&gt;Let’s say the Suspense boundary is showing a spinner for a data fetch. While the fetch is pending, React is rendering lower-priority work. But if the user types or clicks, React wants to respond immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scheduler &lt;strong&gt;interrupts&lt;/strong&gt; the current render.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;pauses&lt;/strong&gt; the Suspense boundary’s work.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;starts&lt;/strong&gt; a higher-priority render (like updating an input).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once high-priority work is done, React &lt;strong&gt;resumes&lt;/strong&gt; the Suspense boundary rendering.&lt;/p&gt;

&lt;p&gt;This pause-and-resume cycle can happen multiple times, which often causes the fallback to appear and disappear several times rapidly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does Suspense sometimes unexpectedly remount or reset state?
&lt;/h2&gt;

&lt;p&gt;One tricky side effect is that when Suspense boundaries retry rendering, React may discard work in progress and start over. This can cause components inside Suspense to unmount and remount ,  resetting local state.&lt;/p&gt;

&lt;p&gt;For example, if you have a form inside a Suspense boundary, users might lose their input unexpectedly if the boundary retries.&lt;/p&gt;

&lt;p&gt;This happens because React’s internal fiber tree abandons the current render and attempts a fresh one when interrupted or when new data arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Suspense boundaries in production
&lt;/h2&gt;

&lt;p&gt;When Suspense boundaries behave oddly, here’s how to get a clearer picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable React DevTools Profiler&lt;/strong&gt;: It shows you when rendering work starts, suspends, and commits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;React.unstable_trace&lt;/code&gt; or React 18’s tracing APIs&lt;/strong&gt;: They help track update flows and see interruptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add logging inside your Suspense fallback and wrapped components&lt;/strong&gt;: Knowing when they mount, unmount, or suspend helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for state resets&lt;/strong&gt;: If state inside Suspense resets unexpectedly, consider moving state above the boundary or using &lt;code&gt;useRef&lt;/code&gt; to preserve values.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical tips to reduce flickers and interruptions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch updates&lt;/strong&gt;: Group multiple setState calls to reduce intermediate renders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;useTransition&lt;/code&gt; wisely&lt;/strong&gt;: Mark non-urgent updates as transitions so React can better prioritize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split Suspense boundaries thoughtfully&lt;/strong&gt;: Smaller boundaries isolate suspensions and reduce widespread UI resets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid putting critical state inside Suspense boundaries that might remount&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you want to dive deeper into the scheduler
&lt;/h2&gt;

&lt;p&gt;React’s scheduler uses priority lanes internally ,  like lanes on a highway ,  to organize and reorder work. High priority lanes are for urgent tasks like user input, while low priority lanes are for background data loading.&lt;/p&gt;

&lt;p&gt;If you’re curious, the React source code and some community tools expose these internals so you can peek under the hood.&lt;/p&gt;

&lt;p&gt;Understanding this helps explain why some updates interrupt others and how React keeps your app responsive even during heavy lifting.&lt;/p&gt;




&lt;p&gt;React’s concurrent rendering isn’t magic, but it’s subtle. That flickering spinner or disappearing UI isn’t a bug ,  it’s React doing its best to balance responsiveness and completeness.&lt;/p&gt;

&lt;p&gt;With this inside view of scheduling, interruptions, and Suspense boundaries, you can debug smarter, design boundaries better, and build smoother experiences for your users.&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/react-concurrent-rendering-scheduling-interruptions-and-debugging-suspense-boundaries" 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>concurrentmode</category>
      <category>scheduler</category>
      <category>suspense</category>
    </item>
    <item>
      <title>How Browsers Handle Keyboard Events: From Raw Input to DOM Events</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:57:23 +0000</pubDate>
      <link>https://dev.to/mspk97/how-browsers-handle-keyboard-events-from-raw-input-to-dom-events-6o8</link>
      <guid>https://dev.to/mspk97/how-browsers-handle-keyboard-events-from-raw-input-to-dom-events-6o8</guid>
      <description>&lt;p&gt;Ever been bitten by a keyboard event bug where your app gets multiple keydown events for a single press, or where input composition (hello, IMEs!) breaks your form? Or maybe you’ve seen different browsers fire keyboard events in different orders and wondered what’s going on.&lt;/p&gt;

&lt;p&gt;I recently spent a frustrating afternoon debugging exactly that kind of issue. The app had subtle bugs: repeated characters flooding inputs, missed keyup events, and weird behavior when users typed accented characters on macOS vs Windows.&lt;/p&gt;

&lt;p&gt;Turns out, keyboard event handling in browsers is a surprisingly intricate dance. It’s not just “key pressed, event fired.” There’s a whole pipeline from the raw OS input, through composition and key repeat, to DOM events that your JavaScript sees.&lt;/p&gt;

&lt;p&gt;Let me walk you through the journey of a single keystroke, from the raw hardware signal all the way to the DOM event listeners that your code hooks into, and why understanding this can save you hours of debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you press a key, the OS speaks first
&lt;/h2&gt;

&lt;p&gt;Your keyboard doesn’t talk directly to the browser. It communicates with the operating system, which interprets raw scan codes and decides what key was pressed, taking into account keyboard layout, modifiers, accessibility settings, and input method editors (IMEs).&lt;/p&gt;

&lt;p&gt;The OS then sends processed key events to the browser. This is why your keyboard layout or system language can change the keys you get in JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser’s input pipeline: raw event to DOM event
&lt;/h2&gt;

&lt;p&gt;Browsers receive low-level key signals from the OS and transform them into the DOM keyboard events you use: &lt;code&gt;keydown&lt;/code&gt;, &lt;code&gt;keypress&lt;/code&gt; (deprecated but still around), and &lt;code&gt;keyup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s the usual event flow for a single physical key press:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;keydown&lt;/strong&gt;: Fired immediately when the key is physically pressed down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;keypress&lt;/strong&gt;: Fired only for keys that produce a character value. This event is deprecated but still emitted in many browsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;input&lt;/strong&gt;: For text input fields, when the key produces a character, the &lt;code&gt;input&lt;/code&gt; event fires to reflect text changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;keyup&lt;/strong&gt;: Fired when the key is released.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But it’s not always this simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key repeat
&lt;/h2&gt;

&lt;p&gt;Holding down a key triggers multiple &lt;code&gt;keydown&lt;/code&gt; and &lt;code&gt;keypress&lt;/code&gt; events due to key repeat. The browser fires these events repeatedly until you release the key, and then a single &lt;code&gt;keyup&lt;/code&gt; is fired.&lt;/p&gt;

&lt;p&gt;This often surprises developers who expect only one &lt;code&gt;keydown&lt;/code&gt; per press. Handling key repeat requires explicit logic if you want to ignore repeats or treat them differently.&lt;/p&gt;

&lt;p&gt;You can check &lt;code&gt;event.repeat&lt;/code&gt; to distinguish repeated keydown events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Composition events for IMEs
&lt;/h2&gt;

&lt;p&gt;Input Method Editors introduce another layer. For complex input like Japanese, Chinese, or accented characters, a single character can require multiple keystrokes before it’s finalized.&lt;/p&gt;

&lt;p&gt;During composition, browsers emit &lt;code&gt;compositionstart&lt;/code&gt;, multiple &lt;code&gt;compositionupdate&lt;/code&gt;, and finally &lt;code&gt;compositionend&lt;/code&gt; events. The DOM does &lt;strong&gt;not&lt;/strong&gt; emit normal &lt;code&gt;keydown&lt;/code&gt;/&lt;code&gt;keypress&lt;/code&gt; events for every raw key during composition, or the text input changes might be incomplete or wrong.&lt;/p&gt;

&lt;p&gt;Handling composition properly is critical for internationalized apps, or you’ll break input for a large user base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Order matters: event sequence and quirks across browsers
&lt;/h2&gt;

&lt;p&gt;The exact event sequence can differ between browsers or platforms. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some browsers fire &lt;code&gt;keypress&lt;/code&gt; after &lt;code&gt;keydown&lt;/code&gt;, others fire it before.&lt;/li&gt;
&lt;li&gt;macOS Safari historically fires &lt;code&gt;keydown&lt;/code&gt; only once per physical press, not repeating with key hold.&lt;/li&gt;
&lt;li&gt;Windows Chrome and Firefox repeat &lt;code&gt;keydown&lt;/code&gt; and &lt;code&gt;keypress&lt;/code&gt; during key hold.&lt;/li&gt;
&lt;li&gt;During IME composition, key events may be suppressed or delayed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These differences make debugging keyboard input across browsers a headache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical debugging tips
&lt;/h2&gt;

&lt;p&gt;If you’re confused why your key handler behaves differently across browsers, try this:&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;window&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="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;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="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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;repeat:&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;repeat&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;window&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;keypress&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;keypress&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;window&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;keyup&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;keyup&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;window&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;compositionstart&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;compositionstart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;window&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;compositionupdate&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;compositionupdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nb"&gt;window&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;compositionend&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;compositionend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test typing normal keys, holding keys down, and using IMEs. Notice the event order, which events fire, and whether &lt;code&gt;repeat&lt;/code&gt; is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your input bugs might be composition bugs
&lt;/h2&gt;

&lt;p&gt;If you see your input event handlers getting unexpected partial text or spamming multiple events during accented character input, check your composition event handling.&lt;/p&gt;

&lt;p&gt;Ignoring composition events can make your app behave like it’s broken when users type non-ASCII characters.&lt;/p&gt;

&lt;p&gt;A common pattern is to ignore key events during composition:&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;let&lt;/span&gt; &lt;span class="nx"&gt;isComposing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;input&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;compositionstart&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isComposing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;input&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;compositionend&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isComposing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;input&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;isComposing&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="c1"&gt;// ignore during composition&lt;/span&gt;
  &lt;span class="c1"&gt;// handle keydown normally&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Keyboard input is deceptively complex. The OS, the browser, and your app all play a part in deciding how key events flow.&lt;/p&gt;

&lt;p&gt;Understanding the event sequence, key repeat, and composition events helps you write more robust input handlers, avoid cross-browser bugs, and build better user experiences, especially for international users.&lt;/p&gt;

&lt;p&gt;Next time you debug a keyboard event mystery, remember: it’s not just your code. It’s a whole pipeline under the hood.&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-events-from-raw-input-to-dom-events" 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>browser</category>
      <category>keyboardevents</category>
      <category>frontend</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Screen Reader Handling of ARIA Live Regions: Timing, Interruptions, and Debugging</title>
      <dc:creator>Srikar Phani Kumar Marti</dc:creator>
      <pubDate>Tue, 28 Jul 2026 11:16:13 +0000</pubDate>
      <link>https://dev.to/mspk97/screen-reader-handling-of-aria-live-regions-timing-interruptions-and-debugging-404p</link>
      <guid>https://dev.to/mspk97/screen-reader-handling-of-aria-live-regions-timing-interruptions-and-debugging-404p</guid>
      <description>&lt;p&gt;Ever had a situation where you update a live notification region in your SPA, but the screen reader stays silent? Or worse, it reads some old message, then skips your update altogether?&lt;/p&gt;

&lt;p&gt;I ran into this exact problem recently while working on an accessibility feature that used ARIA live regions to announce dynamic content changes. The updates were there, the DOM was correct, but screen readers just wouldn’t read the new content reliably.&lt;/p&gt;

&lt;p&gt;Turns out, live region announcements are not as straightforward as just toggling &lt;code&gt;aria-live&lt;/code&gt;. There’s a subtle timing dance behind the scenes, and interruptions can silently swallow your messages. Let me walk you through what I learned debugging this, how screen readers handle live region updates, why timing matters, and practical ways to get your ARIA live announcements working consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weird silence: live region updates don’t always get announced
&lt;/h2&gt;

&lt;p&gt;I had a &lt;code&gt;&amp;lt;div aria-live="polite"&amp;gt;&lt;/code&gt; that I updated with new status messages from an async process. When I tested with NVDA and VoiceOver, the first message would be read, but subsequent updates were sometimes ignored.&lt;/p&gt;

&lt;p&gt;At first, I thought my updates weren’t reaching the DOM or the live region was stale. But inspecting with accessibility tools confirmed everything was in place.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How screen readers process ARIA live regions
&lt;/h2&gt;

&lt;p&gt;Screen readers maintain an internal accessibility tree representing the current UI state. When the DOM changes inside an ARIA live region, the screen reader detects those changes and queues announcements.&lt;/p&gt;

&lt;p&gt;But this queue is subject to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timing thresholds:&lt;/strong&gt; screen readers batch rapid-fire changes to avoid reading an avalanche of messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interruptions:&lt;/strong&gt; a user’s current announcement can block or cancel queued messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change heuristics:&lt;/strong&gt; not all changes trigger announcements, e.g., if the content didn't actually change textually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, NVDA waits a little after an update before announcing, to gather potential additional changes. If you update the live region content multiple times quickly, only the last change might get read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with rapid updates and DOM mutations
&lt;/h2&gt;

&lt;p&gt;Imagine your async process emits several updates in under a second. You might:&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;liveRegion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Step 1 completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;liveRegion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Step 2 completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;liveRegion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All done!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only "All done!" might get announced, because the screen reader batches the changes and only reads the final state.&lt;/p&gt;

&lt;p&gt;Sometimes, if the updates happen too fast, the screen reader might not even detect intermediate changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interruptions: how user focus and speech affect announcements
&lt;/h2&gt;

&lt;p&gt;Screen readers can interrupt or suppress live region announcements if the user is already listening to something else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user is navigating the page with the keyboard or mouse.&lt;/li&gt;
&lt;li&gt;If another alert or live region is currently being read.&lt;/li&gt;
&lt;li&gt;If the screen reader is busy announcing system messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means your live region updates can be silently dropped or delayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging live region announcements
&lt;/h2&gt;

&lt;p&gt;To figure out what’s going on, I tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logging live region content changes&lt;/strong&gt; to confirm updates happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slowing down updates&lt;/strong&gt; with &lt;code&gt;setTimeout&lt;/code&gt; to space out announcements.&lt;/li&gt;
&lt;li&gt;Using &lt;strong&gt;different &lt;code&gt;aria-live&lt;/code&gt; values&lt;/strong&gt; like &lt;code&gt;assertive&lt;/code&gt; and &lt;code&gt;polite&lt;/code&gt; to test priority impacts.&lt;/li&gt;
&lt;li&gt;Testing with multiple screen readers (NVDA, JAWS, VoiceOver) since each has quirks.&lt;/li&gt;
&lt;li&gt;Using browser extensions like &lt;strong&gt;Accessibility Insights&lt;/strong&gt; or &lt;strong&gt;VoiceOver Utility&lt;/strong&gt; to inspect the live region's accessibility tree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One useful trick was creating a small demo page where I manually controlled live region updates via buttons and timers. This let me isolate timing and interruption behaviors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood: what you can do to improve live region reliability
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Throttle or debounce your updates.&lt;/strong&gt; Avoid flooding the live region with rapid changes. Group related updates and announce consolidated messages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clear the live region before updating.&lt;/strong&gt; Setting &lt;code&gt;textContent = ''&lt;/code&gt; briefly before inserting new text can force screen readers to recognize a fresh change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use a hidden off-screen live region element.&lt;/strong&gt; Sometimes toggling visibility or using a separate hidden live region helps isolate announcements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Experiment with &lt;code&gt;aria-atomic&lt;/code&gt;.&lt;/strong&gt; Setting &lt;code&gt;aria-atomic="true"&lt;/code&gt; tells screen readers to read the entire live region content on change, which helps when partial updates confuse them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Match your live region role to the context.&lt;/strong&gt; Use &lt;code&gt;role="alert"&lt;/code&gt; for urgent messages (immediate announcements) and &lt;code&gt;aria-live="polite"&lt;/code&gt; for less disruptive updates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test with real users and multiple screen readers.&lt;/strong&gt; Each has different heuristics and timing rules.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A concrete example: reliable live region updates in a SPA
&lt;/h2&gt;

&lt;p&gt;Here’s a snippet I ended up using for a status message live region:&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;liveRegion&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;statusLiveRegion&lt;/span&gt;&lt;span class="dl"&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;announceStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Clear the region to reset announcer&lt;/span&gt;
  &lt;span class="nx"&gt;liveRegion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Add a small delay before updating content&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;liveRegion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;100&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 brief delay and clearing step forces screen readers to catch the change as new content, not just an update.&lt;/p&gt;

&lt;p&gt;On the HTML side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"statusLiveRegion"&lt;/span&gt;
  &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;
  &lt;span class="na"&gt;aria-atomic=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(1px, 1px, 1px, 1px);"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup worked consistently across NVDA, JAWS, and VoiceOver during my testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this still isn’t perfect
&lt;/h2&gt;

&lt;p&gt;Screen reader implementations vary wildly. Some might ignore live region updates if the user has disabled announcements or if the browser accessibility API behaves differently.&lt;/p&gt;

&lt;p&gt;Also, complex content inside live regions (HTML markup, images, or dynamic widgets) can confuse announcements.&lt;/p&gt;

&lt;p&gt;The best you can do is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep live region content simple and textual.&lt;/li&gt;
&lt;li&gt;Minimize rapid updates.&lt;/li&gt;
&lt;li&gt;Test on your target platforms.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I wish I knew sooner
&lt;/h2&gt;

&lt;p&gt;I wasted hours chasing bugs where live region content was correct but announcements silent. Understanding the importance of timing and interruptions was a game changer.&lt;/p&gt;

&lt;p&gt;Also, don’t assume &lt;code&gt;aria-live&lt;/code&gt; is magic. It’s a hint to screen readers, which have their own rules and priorities.&lt;/p&gt;

&lt;p&gt;Next time you build dynamic live notifications in your app, remember it’s a conversation between your DOM and the screen reader’s internal queue. Getting the timing and content right is how you make that conversation audible.&lt;/p&gt;

&lt;p&gt;Give these tips a try and save yourself the frustration of silent live regions!&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/screen-reader-handling-of-aria-live-regions-timing-interruptions-and-debugging" 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>screenreaders</category>
      <category>aria</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
