<?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: Damiano Ciarla</title>
    <description>The latest articles on DEV Community by Damiano Ciarla (@damianociarla).</description>
    <link>https://dev.to/damianociarla</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%2F4093006%2Fc57dc91c-c96f-4ff0-a8f4-60a01055676c.jpg</url>
      <title>DEV Community: Damiano Ciarla</title>
      <link>https://dev.to/damianociarla</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/damianociarla"/>
    <language>en</language>
    <item>
      <title>DOM order is not always keyboard order — I built a Chromium scanner to visualize it</title>
      <dc:creator>Damiano Ciarla</dc:creator>
      <pubDate>Mon, 24 Aug 2026 22:07:01 +0000</pubDate>
      <link>https://dev.to/damianociarla/dom-order-is-not-always-keyboard-order-i-built-a-chromium-scanner-to-visualize-it-4hd3</link>
      <guid>https://dev.to/damianociarla/dom-order-is-not-always-keyboard-order-i-built-a-chromium-scanner-to-visualize-it-4hd3</guid>
      <description>&lt;p&gt;The DOM can tell you where elements are. It cannot always tell you the exact&lt;br&gt;
route a keyboard user will experience.&lt;/p&gt;

&lt;p&gt;Positive &lt;code&gt;tabindex&lt;/code&gt; values can reorder controls. JavaScript can redirect focus.&lt;br&gt;
Dialogs can fail to restore it. A custom element may contain an open, closed, or&lt;br&gt;
cross-origin boundary. A scroll container can move the active control outside&lt;br&gt;
the evidence captured in the first viewport.&lt;/p&gt;

&lt;p&gt;Static rules are essential, but keyboard navigation is also an interaction. I&lt;br&gt;
wanted a way to observe that interaction and preserve what happened.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;FocusPath&lt;/strong&gt;, an open-source TypeScript tool that follows &lt;code&gt;Tab&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;Shift+Tab&lt;/code&gt; navigation in a real Chromium session, records the observable focus&lt;br&gt;
stops, and generates a portable HTML report with the route drawn over the page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx focuspath https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The live beta is available at&lt;br&gt;
&lt;a href="https://damianociarla.github.io/focuspath/" rel="noopener noreferrer"&gt;damianociarla.github.io/focuspath&lt;/a&gt;,&lt;br&gt;
and the source is on&lt;br&gt;
&lt;a href="https://github.com/damianociarla/focuspath" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gwuafb3w7y83cib2e8u.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gwuafb3w7y83cib2e8u.jpg" alt="FocusPath live scanner showing the completed route for Example Domain" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The question FocusPath answers
&lt;/h2&gt;

&lt;p&gt;FocusPath does not ask only, "Which elements look focusable?"&lt;/p&gt;

&lt;p&gt;It asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Starting from this document, what focus movement can Chromium actually&lt;br&gt;
observe when a keyboard user presses Tab?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For every observed stop, the report records information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sequence number and total Tab presses;&lt;/li&gt;
&lt;li&gt;element identity, selector, role, and computed accessible name;&lt;/li&gt;
&lt;li&gt;traversal-time and final screenshot geometry;&lt;/li&gt;
&lt;li&gt;scroll and clipping contexts;&lt;/li&gt;
&lt;li&gt;computed outline and shadow values;&lt;/li&gt;
&lt;li&gt;deterministic findings linked back to the relevant stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The report also explains why traversal ended: the document was exhausted,&lt;br&gt;
focus stopped moving, a cycle appeared, or a configured safety budget was&lt;br&gt;
reached.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not derive the route from the DOM?
&lt;/h2&gt;

&lt;p&gt;DOM analysis is useful, but several important cases are dynamic.&lt;/p&gt;

&lt;p&gt;Imagine an icon-only button after a custom menu. The menu intercepts Tab,&lt;br&gt;
moves focus internally, and later sends focus to an element outside its DOM&lt;br&gt;
subtree. Sorting elements by source position or &lt;code&gt;tabindex&lt;/code&gt; does not reproduce&lt;br&gt;
that behavior.&lt;/p&gt;

&lt;p&gt;Closed shadow roots and cross-origin frames add another constraint: an external&lt;br&gt;
scanner cannot inspect their internal controls. FocusPath treats these as&lt;br&gt;
opaque boundaries and reports only what can be observed from the host page. It&lt;br&gt;
does not invent internal evidence.&lt;/p&gt;

&lt;p&gt;That distinction became one of the central design rules of the project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Missing evidence must remain missing evidence, not become a confident guess.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  A report should preserve its limits
&lt;/h2&gt;

&lt;p&gt;Visual evidence is persuasive, which also makes it easy to overstate.&lt;/p&gt;

&lt;p&gt;FocusPath reports include the engine, direction, viewport, traversal budgets,&lt;br&gt;
request totals, blocked resource types, capture dimensions, and truncation&lt;br&gt;
state. A screenshot that does not cover the complete source document is marked&lt;br&gt;
as truncated. A stop outside captured evidence remains in the sequence instead&lt;br&gt;
of being silently dropped.&lt;/p&gt;

&lt;p&gt;The scanner and reporter share a hard evidence budget. Saved reports are&lt;br&gt;
runtime-validated before rendering, including counters, step references,&lt;br&gt;
geometry, capture metadata, and JPEG dimensions.&lt;/p&gt;

&lt;p&gt;These constraints make the output less magical and more reviewable.&lt;/p&gt;
&lt;h2&gt;
  
  
  FocusPath complements rule engines
&lt;/h2&gt;

&lt;p&gt;FocusPath is not a replacement for axe, Lighthouse, Accessibility Insights,&lt;br&gt;
screen-reader testing, or manual review.&lt;/p&gt;

&lt;p&gt;Those tools answer different questions. Rule engines are excellent at finding&lt;br&gt;
many deterministic violations in page state. FocusPath concentrates on the&lt;br&gt;
route produced by keyboard traversal and the evidence surrounding it.&lt;/p&gt;

&lt;p&gt;A practical workflow can use both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run established automated rules.&lt;/li&gt;
&lt;li&gt;Generate a FocusPath report for important journeys.&lt;/li&gt;
&lt;li&gt;Inspect the route and any deterministic focus findings.&lt;/li&gt;
&lt;li&gt;Test manually across browsers and assistive technologies.&lt;/li&gt;
&lt;li&gt;Review focus appearance, content, and interaction semantics with people.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No automated tool, including FocusPath, certifies WCAG conformance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Running it locally
&lt;/h2&gt;

&lt;p&gt;FocusPath is ESM-only and currently targets Node.js 24 or newer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create focuspath-report.html&lt;/span&gt;
npx focuspath https://example.com

&lt;span class="c"&gt;# Use a mobile viewport&lt;/span&gt;
npx focuspath localhost:3000 &lt;span class="nt"&gt;--viewport&lt;/span&gt; 390x844

&lt;span class="c"&gt;# Inspect reverse traversal&lt;/span&gt;
npx focuspath example.com &lt;span class="nt"&gt;--direction&lt;/span&gt; reverse

&lt;span class="c"&gt;# Bound traversal explicitly&lt;/span&gt;
npx focuspath example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-steps&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-tab-presses&lt;/span&gt; 320 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-opaque-tab-presses&lt;/span&gt; 160
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI exits with code &lt;code&gt;1&lt;/code&gt; for an error finding, &lt;code&gt;2&lt;/code&gt; when scanning fails, and&lt;br&gt;
&lt;code&gt;0&lt;/code&gt; otherwise, so it can participate in scripted workflows without pretending&lt;br&gt;
that every warning should block a build.&lt;/p&gt;

&lt;p&gt;The library API is available too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scanFocusPath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;focuspath&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateHtmlReport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;focuspath/reporter&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;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;scanFocusPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000&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="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reverse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxTabPresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;240&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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateHtmlReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separate reporter entry point does not load Playwright, which keeps&lt;br&gt;
report-only consumers smaller and easier to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hosted scanner is intentionally narrower
&lt;/h2&gt;

&lt;p&gt;The public beta accepts public HTTP(S) pages and runs one bounded, forward-only&lt;br&gt;
scan. It uses stricter limits than the local CLI, blocks font and media requests&lt;br&gt;
to control resource use, and discloses those restrictions in every report.&lt;/p&gt;

&lt;p&gt;Submitted URLs are validated before scanning, Chromium traffic passes through&lt;br&gt;
a pinned egress proxy, and private, loopback, link-local, and cloud-metadata&lt;br&gt;
destinations are rejected. The service intentionally has no application-level&lt;br&gt;
report store, although infrastructure providers may retain request metadata.&lt;/p&gt;

&lt;p&gt;The local package remains the right choice for private projects, configurable&lt;br&gt;
limits, reverse traversal, and sensitive pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would like feedback on
&lt;/h2&gt;

&lt;p&gt;The current release focuses on deterministic traversal and portable evidence.&lt;br&gt;
The roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a GitHub Action with SARIF annotations;&lt;/li&gt;
&lt;li&gt;comparison between two focus paths;&lt;/li&gt;
&lt;li&gt;focus restoration after dialogs close;&lt;/li&gt;
&lt;li&gt;Firefox and WebKit coverage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am especially interested in pages with complex menus, dialogs, scroll&lt;br&gt;
containers, custom elements, and embedded content. If the report makes an&lt;br&gt;
observable boundary or stop unclear, that is useful feedback.&lt;/p&gt;

&lt;p&gt;Try the &lt;a href="https://damianociarla.github.io/focuspath/" rel="noopener noreferrer"&gt;live scanner&lt;/a&gt;, inspect the&lt;br&gt;
&lt;a href="https://github.com/damianociarla/focuspath" rel="noopener noreferrer"&gt;source&lt;/a&gt;, or install&lt;br&gt;
&lt;a href="https://www.npmjs.com/package/focuspath" rel="noopener noreferrer"&gt;&lt;code&gt;focuspath&lt;/code&gt; from npm&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The goal is not to replace human accessibility judgment. It is to make one&lt;br&gt;
important part of the interaction easier to see, reproduce, and discuss.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
