Why tree views demand special attention

Tree views present a well-known accessibility challenge. They pack nested navigation, expandable branches, and selectable nodes into a single interface — and they must behave predictably across a wide range of assistive technologies. GitHub's file tree is a prime example of such a component in production. The approach taken to make it accessible offers useful patterns for others building similar interfaces.

Browsing files on Primer's design repository. A tree view showing the repositories directory structure occupies a quarter of the screen. The other three quarters are taken up by the content of the content subdirectory. The tree view shows expanded and collapsed directories, as well as files nested at multiple levels of depth.

On GitHub, the tree view mirrors the folder-and-file hierarchy familiar from operating system file explorers. It's a list of lists, and that foundation shapes the entire accessibility strategy.

Grounding the interaction model in Windows File Explorer

For components with complex interaction requirements, it pays to model behavior on patterns people already know. Windows File Explorer's tree view was the natural reference here, given that Windows is the dominant platform among desktop screen reader users. Testing with NVDA and JAWS against the native implementation clarified how to handle focus management, keypresses, and expected announcements.

A Windows 11 File Explorer window showing a tree view and a list of subdirectories that one of its folders contains. The tree view demonstrates how the C drive contains multiple nested folders to organize its content.

The ARIA Authoring Practices Guide (APG) can be a helpful starting point for thinking through the overall pattern. However, it is no longer recognized by the W3C as a formal document, so its code suggestions warrant scrutiny before being adopted wholesale.

Semantic structure first

Because a tree view is inherently a nested list, GitHub's implementation builds on ul and li elements rather than generic divs or spans.

<ul>
  <li>
    <ul>
      <li>.github/</li>
      <li>source/</li>
      <li>test/</li>
    </ul>
  </li>
  <li>.gitignore</li>
  <li>README.md</li>
</ul>

This choice has several practical benefits:

  • It produces a more meaningful accessibility tree for screen readers.
  • It reduces the maintenance burden and the risk of regressions when updates are made.
  • It improves interoperability across browsers, operating systems, and assistive technology combinations.

Using semantic elements also improves support beyond the most common screen reader pairings. Testing showed better results on less-common assistive technologies, including some running on lower-power devices like entry-level Android smartphones.

There's another benefit: Forced Color Mode. Semantic elements map to native operating system UI patterns, so the heuristics browsers use for forced colors recognize them without extra work. Recreating that with div and span elements would require manual styling and ongoing maintenance.

One caveat worth noting: GitHub does not currently virtualize its file trees. If that ever changes, this architectural decision would need revisiting, since virtualized rendering fundamentally changes how the accessibility tree and focus are managed.

Composite widget behavior

Treating the tree view as a composite widget means the entire component requires a single tab stop. That matters for real-world use. A repository can contain hundreds of files across dozens of directories; without this treatment, keyboard users would need to press Tab through every node to move past the tree and reach the rest of the page. The composite approach also allows role="tree" and role="treeitem" to describe the structure properly, which HTML alone cannot express.

Given that the file tree is a primary way to navigate repository content, the component is also wrapped in a nav landmark. This helps people using screen reader landmark navigation move directly to the file tree — particularly useful on larger pages with many sections. This does not mean every tree view should be a landmark; it depends on whether the tree serves as a primary navigation mechanism.

Managing focus with a roving tabindex

A roving tabindex is the mechanism behind the single-tab-stop behavior. Each element in the tree gets tabindex="-1", and when someone navigates through the tree with the keyboard, the element currently in focus is updated to tabindex="0". Focus "roves" through the tree in response to keypresses.

<li tabindex="-1">File 1</li>
<li tabindex="-1">File 2</li>
<li tabindex="0">File 3</li>
<li tabindex="-1">File 4</li>

GitHub evaluated aria-activedescendant as an alternative, but it had issues with VoiceOver on macOS and iOS, so the roving tabindex approach was preferred.

Enhancing the semantics with ARIA

The semantic foundation provides structure, but ARIA is still required to communicate tree-specific semantics. The implementation follows APG recommendations for roles and properties:

  • role="tree" on the root ul element
  • role="treeitem" on each li element
  • role="group" on nested ul elements to group child nodes under their parent
  • aria-expanded on branch nodes, set to true or false depending on state
  • aria-selected to mark the node that holds the active selection for keyboard interaction

The pattern here is not a case of "ARIA is bad" — these roles are necessary because HTML lacks native tree semantics.

Two additional declarations were added beyond the APG guidance:

  • aria-hidden="true" on SVG icons (such as folder and file glyphs) so their contents are not announced
  • aria-current="true" on the selected node, which helps when a tree node is deep-linked via URL fragment

Keyboard navigation expectations

Operating a tree view feels different from operating a typical list of links. People expect directional arrow keys to move selection without leaving the tree. File Explorer's model establishes a set of behaviors that GitHub mirrors:

  • Tab — Moves focus into and out of the tree; one tab stop for the whole component.
  • Enter — On a branch: opens it. On a leaf: opens its target.
  • Down / Up — Moves selection to the next or previous visible node without expanding or collapsing anything.
  • Right — On a collapsed branch: expands it and keeps selection. On an expanded branch: moves into its first child.
  • Left — On an expanded branch: collapses it. On a collapsed branch or a leaf: moves focus to the parent.
  • Home / End — Jumps selection to the first or last node in the tree.

The implementation also supports typeahead: if a user types characters while the tree is focused, selection moves to the node closest to the current selection whose name matches the typed text. This mirrors Windows File Explorer behavior.

Because tree view nodes are not anchor elements, they don't natively support the actions users associate with links — like opening in a new tab or middle-clicking to open a new window. GitHub listens for middle clicks and Ctrl+Enter keypresses specifically to replicate these behaviors for tree items that represent files.

Handling loading and error states

Tree views in GitHub sometimes need to fetch content after a branch is expanded — and unlike a static file listing, the amount of content may not be known up front. When a branch is expanded and a placeholder is shown while content loads, accessibility is maintained in two ways.

A directory called, 'src' that is selected and in an expanded that. It contains a single leaf node that contains a loading spinner with a label of 'Loading…".

First, live region announcements tell the screen reader what is happening. The text differs based on what is known:

  • When the number of nodes is known, the announcement reads "Loading {x} items."
  • When the number is unknown, it simply reads "Loading…"
  • When nothing loads, it reads "{branch node name} is empty."

Second, focus is moved in a way that keeps the user's place in the tree predictable:

  • If focus is on a loading placeholder and content arrives, focus moves to the first of the newly loaded nodes.
  • If a branch contains no content, focus goes back to the branch node, and its aria-expanded value is removed.

If fetching content fails — due to network degradation or a system outage — the error is surfaced through a standard dialog component, since anything else would create untested and likely unreliable behavior.

Closing the assistive technology gap

Complex interaction patterns are prone to interoperability issues, so it's critical to validate the implementation with real assistive technology. Two adjustments proved necessary in our testing:

Declare depth with aria-level

Screen readers naturally report the nesting depth of standard list items. A li inside a ul nested three levels deep gets announced accordingly. To reproduce this behavior for tree view nodes, we had to set aria-level="3" explicitly on each li element. This resolved correct depth announcements across multiple assistive technologies we tested.

Pin the accessible name to the li

A node's accessible name is usually derived from the text directly inside the li:

<li>README.md</li>

VoiceOver on macOS and iOS did not honor this, likely due to the complexity of each node's internal DOM. We solved it with aria-labelledby, pointing to the id on the text element:

<li aria-labelledby="readme-md">
  <div>
   <!-- Icon -->
  </div>
  <div id="readme-md">
    README.md
  </div>
</li>

This ensures the accessible name is announced when the li receives focus and that the announcement matches what is visibly rendered.

Next steps in the component's evolution

We are actively prototyping two extensions to better serve GitHub's users:

Browsers provide built-in behaviors for anchors, like copying the URL. We want to replace our JavaScript-based middle-click handler with a native solution that does not compromise assistive technology support or interoperability.

Multiple actions per node

The tree view pattern assumes navigation and activation are the only user intents. GitHub workflows demand more actions per node, and we're exploring ways to support that. This is an opportunity to push the web's tree view construct forward.

Lessons from the process

Building an accessible tree view requires sustained testing and iteration. The effort is justified: it ensures a core GitHub surface works for everyone, regardless of device or ability. We're sharing these considerations in hopes they help others working on similar components. If you encounter issues with our tree view and assistive technology, please let us know — your reports directly shape our accessibility work.