Accessible Tree Table with Nested Rows — Free JS Snippet

Accessible Tree Table with Expandable Nested Rows · Tables · Plain HTML, CSS & JS · Live preview

CategoryTables

What's included

Features

Expandable nested rows with indentation from a stored depth
Folder sizes computed as a recursive roll-up of descendants
Rendering as a pure function of the tree, open set and filter
Filter keeps ancestors of every match visible and opens them
Match highlighting with HTML-escaped output
WAI-ARIA treegrid roles with aria-level, aria-expanded, aria-posinset and aria-setsize
Roving tabindex so the whole tree is one tab stop
Full keyboard model: arrows, Home, End, Enter, Space

About this UI Snippet

Accessible Tree Table with Expandable Rows — HTML, CSS & JavaScript

Screenshot of the Accessible Tree Table with Expandable Nested Rows snippet rendered live

A tree table combines two structures that browsers do not natively combine: the columns of a table and the expandable hierarchy of a tree. File explorers, chart-of-accounts screens, organisation charts and nested task lists all use it. Building one that merely looks right is easy — indent some rows and toggle their visibility. Building one that behaves correctly for keyboard and screen-reader users is where the work is, and this snippet is structured around that.

The data model does two things worth noticing. Each node stores its parent and its depth, assigned once by a prep() pass, so indentation is a level times 22 pixels and "go to parent" is a property lookup. And folder sizes are a roll-up: a folder's size is the sum of everything beneath it, computed recursively when the tree is prepared, which is how file explorers show that a folder is 60 KB before you open it. Expansion state is kept separately in a Set of open folder ids, so rendering is a pure function of the tree, the open set and the current filter — call render() and the DOM is rebuilt to match.

Only currently visible rows are produced. A recursive walk emits a node and descends into its children only if the folder is open, which yields a flat list that maps straight onto table rows. Filtering has one subtle rule that most implementations get wrong: a node matches if its own name matches or any descendant does. Without the descendant check, searching for "Row" would hide the folders containing Row.tsx and the hit would appear orphaned at the top level. With it, ancestors of every match stay visible and are forced open while a filter is active, and the matched text is highlighted.

The accessibility model follows the WAI-ARIA treegrid pattern. The container has role="treegrid" and rows carry aria-level, aria-posinset, aria-setsize and, for folders, aria-expanded, so a screen reader can announce "Table, folder, expanded, level 3, 2 of 8". Focus uses a roving tabindex — exactly one row is tabindex 0 — so the whole grid is a single tab stop. The keyboard handler implements the standard interactions: Up and Down move between rows, Right opens a closed folder or steps into its first child, Left closes an open folder or jumps to the parent, Home and End go to the ends, and Enter or Space toggles. Chevron rotation is CSS-only, driven from aria-expanded.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to add lazy loading of children from an API, drag-and-drop to move files between folders, or multi-select with checkboxes and tri-state parents.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an accessible tree table (role="treegrid") in vanilla JavaScript for a nested file structure.

Requirements:
- Prepare the tree once: assign each node an id, parent and level, and compute each folder's size as the recursive sum of its children.
- Keep expanded folder ids in a Set; render() flattens only visible rows and rebuilds the DOM, indenting by level.
- Add a filter that matches a node if its name or any descendant matches, force-opens ancestors while filtering, and highlights the match with escaped HTML.
- Add aria-level, aria-posinset, aria-setsize and aria-expanded on rows, a roving tabindex, and the treegrid keyboard model (Up/Down, Right to open or enter, Left to close or go to parent, Home/End, Enter/Space).
- Provide Expand all and Collapse all buttons and a rotating chevron driven by aria-expanded.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="tt-wrap">
  <div class="tt-bar">
    <input id="ttFilter" type="search" placeholder="Filter files and folders..." aria-label="Filter the tree">
    <button type="button" id="ttExpand">Expand all</button>
    <button type="button" id="ttCollapse">Collapse all</button>
  </div>
  <div class="tt-grid" role="treegrid" aria-label="Project files" id="ttGrid">
    <div class="tt-head" role="row"><span role="columnheader">Name</span><span role="columnheader">Type</span><span role="columnheader">Modified</span><span role="columnheader" class="r">Size</span></div>
    <div id="ttBody"></div>
  </div>
  <p class="tt-help" id="ttHelp">Arrow keys move, <kbd>&rarr;</kbd> opens a folder, <kbd>&larr;</kbd> closes it or jumps to its parent, <kbd>Home</kbd>/<kbd>End</kbd> go to the first and last row.</p>
</div>

Step by step

How to Use

  1. 1
    Expand and collapseClick a folder or its arrow to open it. Folder sizes are the total of everything inside, even when collapsed.
  2. 2
    Use the keyboardTab into the tree, then use the arrow keys. Right opens a folder, Left closes it or jumps to its parent.
  3. 3
    Expand allUse Expand all and Collapse all to open or fold the whole hierarchy at once.
  4. 4
    FilterType "Row" or "hook". Matching files appear with their parent folders kept open and the match highlighted.
  5. 5
    Check the semanticsInspect the rows to see aria-level, aria-expanded, aria-posinset and aria-setsize on each one.

Real-world uses

Common Use Cases

File and asset browsers
Show nested files with rolled-up sizes. For a flat, very large list see the virtualized 100k-row table.
Chart of accounts and budgets
Nest categories under parents with subtotals.
Org charts and permission trees
Explore reporting lines or nested roles in a table with columns.
Learning the treegrid pattern
A complete, working example of the WAI-ARIA treegrid keyboard interactions.

Got questions?

Frequently Asked Questions

A tree is a hierarchical list. A treegrid is a hierarchy laid out in rows and columns, so each node can show multiple cells such as type, date and size.

It separates the state from the DOM, so render() can rebuild the view from the data, the open set and the filter every time.

If only the node's own name is tested, folders that contain a match are hidden and the result appears without context. Checking descendants keeps the ancestors visible.

Only one row has tabindex 0 and the others have -1, so the tree is a single tab stop, and arrow keys move focus within it.

Each folder's size is the sum of its children's sizes, computed recursively once when the tree is prepared.

Mark folders as having unloaded children, fetch them on first expand, insert the nodes, then call render() and set aria-busy while loading.

Yes. Use the JSX, Vue, Angular or Tailwind export buttons on this page. It uses no library, so port the tree data and the open-folder Set into component state, render the visible rows from them, and keep the roving tabindex and keyboard handler on the treegrid element.