Accessibility pattern · Data display

Tree view

The tree is one stop in the Tab order. Up and Down walk the visible items, Right opens a folder or steps into it, and Left closes it or climbs to its parent. Nested groups give every item its level and its place in its set, so a screen reader can say "level 3, 1 of 3" without extra attributes.

WCAG criteria
6
Keyboard rules
7
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Project files

storefront

  • src
    • components
      • Button.tsx
      • Card.tsx
      • Dialog.tsx
    • main.ts
  • package.json
  • README.md
  • vite.config.ts

Details

Button.tsx

React component

Size
2.4 KB
Modified
4 Oct 2026
Location
storefront / src / components

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Arrow Down or Arrow UpMoves to the next or previous item you can see, without opening or closing anything.
Arrow RightOpens a closed folder; on an open folder, moves to its first item.
Arrow LeftCloses an open folder; on anything else, moves to the folder that holds it.
Home or EndMoves to the first or last item you can see.
Enter or SpaceSelects the focused item and shows it in the details pane.
*Opens every folder at the same level as the focused one.
A–ZMoves to the next item whose name starts with the letters typed.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the treeProject files, tree. Button.tsx, selected, level 3, 1 of 3
Arrow Down moves to the next fileCard.tsx, not selected, level 3, 2 of 3
Focus reaches a closed folderpages, collapsed, level 2, 2 of 4
Arrow Right opens itExpanded
Enter selects the focused fileCard.tsx, selected

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-tree" data-ap-tree>
  <div class="ap-tree__top">
    <h3 class="ap-tree__name" id="tree-name">Project files</h3>
    <p class="ap-tree__sub" translate="no">storefront</p>
  </div>
  <div class="ap-tree__body">
    <ul class="ap-tree__list" role="tree" aria-labelledby="tree-name" data-root="storefront">
      <li class="ap-tree__item" role="treeitem" aria-expanded="true" aria-labelledby="tree-src" tabindex="-1" data-kind="Folder" data-size="4 items" data-modified="4 Oct 2026">
        <span class="ap-tree__row"><svg class="ap-tree__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9.5 6.5 5.5 5.5-5.5 5.5"/></svg><svg class="ap-tree__icon ap-tree__icon--shut" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.6l2 2.2h7.4a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg><svg class="ap-tree__icon ap-tree__icon--open" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 17V7.5a2 2 0 0 1 2-2h3.6l2 2.2h6.4a2 2 0 0 1 2 2v1"/><path d="M3.6 18.2 6.2 11.6a1.6 1.6 0 0 1 1.5-1h12.8l-2.7 7.3a1.6 1.6 0 0 1-1.5 1H5a1.4 1.4 0 0 1-1.4-.7Z"/></svg><span class="ap-tree__label" id="tree-src" translate="no">src</span></span>
        <ul class="ap-tree__group" role="group">
          <li class="ap-tree__item" role="treeitem" aria-expanded="true" aria-labelledby="tree-src-components" tabindex="-1" data-kind="Folder" data-size="3 items" data-modified="4 Oct 2026">
            <span class="ap-tree__row"><svg class="ap-tree__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9.5 6.5 5.5 5.5-5.5 5.5"/></svg><svg class="ap-tree__icon ap-tree__icon--shut" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.6l2 2.2h7.4a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg><svg class="ap-tree__icon ap-tree__icon--open" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 17V7.5a2 2 0 0 1 2-2h3.6l2 2.2h6.4a2 2 0 0 1 2 2v1"/><path d="M3.6 18.2 6.2 11.6a1.6 1.6 0 0 1 1.5-1h12.8l-2.7 7.3a1.6 1.6 0 0 1-1.5 1H5a1.4 1.4 0 0 1-1.4-.7Z"/></svg><span class="ap-tree__label" id="tree-src-components" translate="no">components</span></span>
            <ul class="ap-tree__group" role="group">
              <li class="ap-tree__item" role="treeitem" tabindex="0" aria-selected="true" data-kind="React component" data-size="2.4 KB" data-modified="4 Oct 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">Button.tsx</span></span>
              </li>
              <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="React component" data-size="1.8 KB" data-modified="28 Sep 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">Card.tsx</span></span>
              </li>
              <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="React component" data-size="3.9 KB" data-modified="1 Oct 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">Dialog.tsx</span></span>
              </li>
            </ul>
          </li>
          <li class="ap-tree__item" role="treeitem" aria-expanded="false" aria-labelledby="tree-src-pages" tabindex="-1" data-kind="Folder" data-size="3 items" data-modified="2 Oct 2026">
            <span class="ap-tree__row"><svg class="ap-tree__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9.5 6.5 5.5 5.5-5.5 5.5"/></svg><svg class="ap-tree__icon ap-tree__icon--shut" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.6l2 2.2h7.4a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg><svg class="ap-tree__icon ap-tree__icon--open" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 17V7.5a2 2 0 0 1 2-2h3.6l2 2.2h6.4a2 2 0 0 1 2 2v1"/><path d="M3.6 18.2 6.2 11.6a1.6 1.6 0 0 1 1.5-1h12.8l-2.7 7.3a1.6 1.6 0 0 1-1.5 1H5a1.4 1.4 0 0 1-1.4-.7Z"/></svg><span class="ap-tree__label" id="tree-src-pages" translate="no">pages</span></span>
            <ul class="ap-tree__group" role="group" hidden>
              <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="React component" data-size="5.2 KB" data-modified="2 Oct 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">Checkout.tsx</span></span>
              </li>
              <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="React component" data-size="3.1 KB" data-modified="19 Sep 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">Home.tsx</span></span>
              </li>
              <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="React component" data-size="4.6 KB" data-modified="30 Sep 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">Orders.tsx</span></span>
              </li>
            </ul>
          </li>
          <li class="ap-tree__item" role="treeitem" aria-expanded="false" aria-labelledby="tree-src-styles" tabindex="-1" data-kind="Folder" data-size="2 items" data-modified="27 Sep 2026">
            <span class="ap-tree__row"><svg class="ap-tree__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9.5 6.5 5.5 5.5-5.5 5.5"/></svg><svg class="ap-tree__icon ap-tree__icon--shut" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.6l2 2.2h7.4a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg><svg class="ap-tree__icon ap-tree__icon--open" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 17V7.5a2 2 0 0 1 2-2h3.6l2 2.2h6.4a2 2 0 0 1 2 2v1"/><path d="M3.6 18.2 6.2 11.6a1.6 1.6 0 0 1 1.5-1h12.8l-2.7 7.3a1.6 1.6 0 0 1-1.5 1H5a1.4 1.4 0 0 1-1.4-.7Z"/></svg><span class="ap-tree__label" id="tree-src-styles" translate="no">styles</span></span>
            <ul class="ap-tree__group" role="group" hidden>
              <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="Stylesheet" data-size="1.2 KB" data-modified="27 Sep 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">global.css</span></span>
              </li>
              <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="Stylesheet" data-size="2.7 KB" data-modified="25 Sep 2026">
                <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">tokens.css</span></span>
              </li>
            </ul>
          </li>
          <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="TypeScript file" data-size="0.6 KB" data-modified="12 Sep 2026">
            <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">main.ts</span></span>
          </li>
        </ul>
      </li>
      <li class="ap-tree__item" role="treeitem" aria-expanded="false" aria-labelledby="tree-public" tabindex="-1" data-kind="Folder" data-size="2 items" data-modified="8 Sep 2026">
        <span class="ap-tree__row"><svg class="ap-tree__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9.5 6.5 5.5 5.5-5.5 5.5"/></svg><svg class="ap-tree__icon ap-tree__icon--shut" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.6l2 2.2h7.4a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg><svg class="ap-tree__icon ap-tree__icon--open" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 17V7.5a2 2 0 0 1 2-2h3.6l2 2.2h6.4a2 2 0 0 1 2 2v1"/><path d="M3.6 18.2 6.2 11.6a1.6 1.6 0 0 1 1.5-1h12.8l-2.7 7.3a1.6 1.6 0 0 1-1.5 1H5a1.4 1.4 0 0 1-1.4-.7Z"/></svg><span class="ap-tree__label" id="tree-public" translate="no">public</span></span>
        <ul class="ap-tree__group" role="group" hidden>
          <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="SVG image" data-size="1.1 KB" data-modified="8 Sep 2026">
            <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">favicon.svg</span></span>
          </li>
          <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="Text file" data-size="68 bytes" data-modified="8 Sep 2026">
            <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">robots.txt</span></span>
          </li>
        </ul>
      </li>
      <li class="ap-tree__item" role="treeitem" aria-expanded="false" aria-labelledby="tree-tests" tabindex="-1" data-kind="Folder" data-size="2 items" data-modified="3 Oct 2026">
        <span class="ap-tree__row"><svg class="ap-tree__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9.5 6.5 5.5 5.5-5.5 5.5"/></svg><svg class="ap-tree__icon ap-tree__icon--shut" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.6l2 2.2h7.4a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg><svg class="ap-tree__icon ap-tree__icon--open" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 17V7.5a2 2 0 0 1 2-2h3.6l2 2.2h6.4a2 2 0 0 1 2 2v1"/><path d="M3.6 18.2 6.2 11.6a1.6 1.6 0 0 1 1.5-1h12.8l-2.7 7.3a1.6 1.6 0 0 1-1.5 1H5a1.4 1.4 0 0 1-1.4-.7Z"/></svg><span class="ap-tree__label" id="tree-tests" translate="no">tests</span></span>
        <ul class="ap-tree__group" role="group" hidden>
          <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="TypeScript file" data-size="1.9 KB" data-modified="3 Oct 2026">
            <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">button.test.ts</span></span>
          </li>
          <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="TypeScript file" data-size="3.4 KB" data-modified="2 Oct 2026">
            <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">checkout.test.ts</span></span>
          </li>
        </ul>
      </li>
      <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="JSON file" data-size="1.3 KB" data-modified="29 Sep 2026">
        <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">package.json</span></span>
      </li>
      <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="Markdown file" data-size="2.2 KB" data-modified="15 Sep 2026">
        <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">README.md</span></span>
      </li>
      <li class="ap-tree__item" role="treeitem" tabindex="-1" data-kind="TypeScript file" data-size="0.4 KB" data-modified="10 Sep 2026">
        <span class="ap-tree__row"><span class="ap-tree__spacer"></span><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><span class="ap-tree__label" translate="no">vite.config.ts</span></span>
      </li>
    </ul>

    <section class="ap-tree__details" aria-labelledby="tree-details-name" data-ap-details>
      <h3 class="ap-tree__pane-name" id="tree-details-name">Details</h3>
      <div class="ap-tree__preview">
        <span class="ap-tree__tile" aria-hidden="true"><svg class="ap-tree__icon ap-tree__icon--file" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h7l4.5 4.5v12.5h-11.5Z"/><path d="M13.5 3.5V8H18"/></svg><svg class="ap-tree__icon ap-tree__icon--shut" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7.5a2 2 0 0 1 2-2h3.6l2 2.2h7.4a2 2 0 0 1 2 2v7.8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/></svg></span>
        <div>
          <p class="ap-tree__file" translate="no" data-ap-file>Button.tsx</p>
          <p class="ap-tree__kind" data-ap-kind>React component</p>
        </div>
      </div>
      <dl class="ap-tree__facts">
        <div><dt>Size</dt><dd data-ap-size>2.4 KB</dd></div>
        <div><dt>Modified</dt><dd data-ap-modified>4 Oct 2026</dd></div>
        <div><dt>Location</dt><dd translate="no" data-ap-where>storefront / src / components</dd></div>
      </dl>
    </section>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Nested role=group lists give every item its level, its position and the size of its set; folders expose aria-expanded.

  • 1.4.1 Use of Color Level A

    Selection is shown by weight, a bar at the row's start and the details pane, not by the tint alone.

  • 2.1.1 Keyboard Level A

    Every action has a key: arrows to move, open and close, Enter or Space to select, * to open a level, letters to jump.

  • 2.4.3 Focus Order Level A

    The tree is one tab stop that returns to the last item used; the details pane follows it in the reading order.

  • 2.4.7 Focus Visible Level AA

    The focused row shows a two-pixel ring, separate from the selected row's tint, so both can be seen at once.

  • 4.1.2 Name, Role, Value Level A

    Items have role=treeitem with a name from their label, folders carry aria-expanded, and the chosen item has aria-selected.

Usage

When to use it

Use it

  • Hierarchies people open and close as they go, like folders, a site map or nested categories.
  • Picking one item from a deep structure while seeing where it sits.

Use something else

  • Site navigation: a nav landmark with nested lists of links is simpler and works like the rest of the web.
  • Two levels or fewer: an accordion or grouped list says the same with less to learn.
  • Tabular data in a hierarchy, with columns per row: that is a treegrid.

Common failures

How it usually goes wrong

  • Every item in the Tab order

    A tree with forty visible items should not take forty Tab presses to leave. One tab stop with arrow keys inside is the model screen reader users know.

  • aria-selected="false" on every item

    In a single-select tree, marking all the others not selected makes screen readers repeat it on each row. Only the chosen item carries aria-selected.

  • Folders named by everything inside them

    A parent item's text includes its children, so its name from content can run to dozens of files. aria-labelledby points each folder at its own label.

  • Collapsed items left reachable

    Children hidden only visually are still read and still found by the arrow keys. The hidden attribute on the group takes them out for everyone.

  • Focus and selection that look the same

    If the focus ring and the selected row share one style, people cannot tell where they are from what they chose. Here the ring and the tinted bar differ.

  • Icons announced as images

    Folder and file icons repeat the item's kind. They are aria-hidden, and the name says what the item is.

Notes

Building it

  • With nested ul role=group, browsers work out aria-level, aria-setsize and aria-posinset; set them yourself only when the tree is a flat list or loads items lazily.
  • The option above makes selection follow focus, as in many file managers; leave it off when selecting does something slow, such as loading a preview.
  • Type-ahead matches from the item after the focused one and wraps; letters typed within half a second build a longer prefix.
  • The details pane is not a live region: the tree item already announces that it is selected, and the pane is there to read after the tree.
  • Clicking a folder opens or closes it and selects it, as in most code editors; the arrow keys keep opening and moving separate.

Sources: WAI-ARIA Authoring Practices: Tree View · WAI-ARIA Authoring Practices: File Directory Treeview Example

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction