Skip to content

File Tree

A file and folder tree built on Tree, from a list of nodes. Folders show open and closed icons, files get an icon by extension, and each row can carry a size or a status badge.

Datav0.1.0experimentalWCAG 2.2 AAView spec
src
index.ts
package.json
LICENSE
import { FileTree, type FileTreeNode } from "@rdloom/react";

const items: FileTreeNode[] = [
  {
    id: "src",
    name: "src",
    children: [
      {
        id: "components",
        name: "components",
        children: [
          { id: "card", name: "card.tsx" },
          { id: "menu", name: "menu.tsx" },
        ],
      },
      { id: "index", name: "index.ts" },
      { id: "logo", name: "logo.png" },
    ],
  },
  { id: "docs", name: "docs", children: [{ id: "readme", name: "README.md" }] },
  { id: "package", name: "package.json" },
  { id: "license", name: "LICENSE" },
];

export default function FileTreeBasicExample() {
  return (
    <div className="flex w-full justify-center">
      <div className="w-72">
        <FileTree label="Project files" items={items} defaultExpandedKeys={["src"]} />
      </div>
    </div>
  );
}

Installation

npx rdloom add file-tree

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/file-tree.json

Works, but without upgrade tracking.

Usage

import { FileTree, type FileTreeNode } from "@rdloom/react";

<div className="flex w-full justify-center">
  <div className="w-72">
    <FileTree label="Project files" items={items} defaultExpandedKeys={["src"]} />
  </div>
</div>

With sizes and status

app
page.tsx
layout.tsx
styles.css
notes.txt
config.json
import { FileTree, type FileTreeNode } from "@rdloom/react";

const items: FileTreeNode[] = [
  {
    id: "app",
    name: "app",
    children: [
      { id: "page", name: "page.tsx", size: "2.1 KB", status: "modified" },
      { id: "layout", name: "layout.tsx", size: "1.4 KB" },
      { id: "styles", name: "styles.css", size: "6.8 KB", status: "added" },
    ],
  },
  { id: "notes", name: "notes.txt", size: "312 B", status: "deleted" },
  { id: "config", name: "config.json", size: "0.9 KB" },
];

export default function FileTreeWithSizesAndStatusExample() {
  return (
    <div className="flex w-full justify-center">
      <div className="w-80">
        <FileTree label="Changed files" items={items} defaultExpandedKeys={["app"]} />
      </div>
    </div>
  );
}

Controlled

assets
hero.webp
icon.svg
main.py
README.md

Selected: main. Last opened: none.

import { useState } from "react";
import { FileTree, type FileTreeNode } from "@rdloom/react";

const items: FileTreeNode[] = [
  { id: "assets", name: "assets", children: [{ id: "hero", name: "hero.webp" }, { id: "icon", name: "icon.svg" }] },
  { id: "main", name: "main.py" },
  { id: "readme", name: "README.md" },
];

export default function FileTreeControlledExample() {
  const [selected, setSelected] = useState<Set<string>>(new Set(["main"]));
  const [open, setOpen] = useState<Set<string>>(new Set(["assets"]));
  const [last, setLast] = useState("");
  return (
    <div className="flex w-full flex-col items-center gap-3">
      <div className="w-72">
        <FileTree
          label="Controlled files"
          items={items}
          selectedKeys={selected}
          onSelectionChange={(keys) => setSelected(keys === "all" ? new Set() : new Set([...keys].map(String)))}
          expandedKeys={open}
          onExpandedChange={(keys) => setOpen(new Set([...keys].map(String)))}
          onAction={(key) => setLast(String(key))}
        />
      </div>
      <p className="text-sm text-[var(--rd-color-text-muted)]" aria-live="polite">
        Selected: {[...selected].join(", ") || "nothing"}. Last opened: {last || "none"}.
      </p>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
labelrequired

Accessible name of the tree, e.g. "Project files".

stringnone
itemsrequired

The nodes. A node with children is a folder; the id is its key and is unique across the whole tree.

FileTreeNode[]none
selectionMode

Whether rows can be selected.

"none" | "single" | "multiple""single"
selectedKeys

Controlled selection, by node id.

"all" | Iterable<Key>none
defaultSelectedKeys

Initial selection when uncontrolled.

"all" | Iterable<Key>none
onSelectionChange

Called with the selected node ids.

(keys: "all" | Set<Key>) => voidnone
expandedKeys

Controlled set of open folders, by id.

Iterable<Key>none
defaultExpandedKeys

Folders open at first.

Iterable<Key>none
onExpandedChange

Called when folders are opened or closed.

(keys: Set<Key>) => voidnone
onAction

Called when a row is activated with Enter or a double-click.

(key: Key) => voidnone

Accessibility

Role tree, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab moves into the tree, on one row, and out again
  • Up and Down arrows move between visible rows
  • Right arrow opens a closed folder, or moves to its first child; Left arrow closes it, or moves to its parent
  • Home and End jump to the first and last visible row
  • Enter activates the row (onAction); Space selects it when selection is on
  • Typing a letter jumps to the next row starting with it

Screen readers announce

  • Entering announces the tree name, the row, its level and position, and whether a folder is open
  • A row with a status is read as its name followed by the status
  • Opening or closing a folder announces the new state

What your code must do

  • Built on Tree: a named tree whose rows expose aria-level, aria-expanded (folders) and aria-selected (when selectable)
  • File and folder icons are decoration (aria-hidden); the name is the text of the row
  • A status such as modified is part of the row accessible name, not only a coloured badge, and the badge shows a letter as well as a colour
  • Sizes are text you supply, already formatted for your locale
  • Selection is shown by a filled row and aria-selected, not only colour

Guidelines

Use it when

  • Showing a project's files and folders
  • Picking a file from a hierarchy, with an optional size or change status

Avoid it when

  • Flat file lists: use a ListBox or Table
  • Folders with thousands of children loaded at once

Don't

  • Reusing the same id for two nodes
  • Showing the status only by colour
  • Using the file extension as the only way to tell what a file is

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-default
  • --rd-color-surface-subtle
  • --rd-color-surface-selected
  • --rd-color-border-default
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-focus-ring
  • --rd-radius-control