File tree
A tree of nested folders and files, for browsing the contents of an artifact or repository. For links between the areas of an app, use Side navigation.
Code
import {FileTree, type FileTreeItemState, type FileTreeNode} from '@earendil-works/elanor-react';
import {useCallback, useState, type MouseEvent} from 'react';
const items: FileTreeNode[] = [
{
id: 'docs',
name: 'docs',
children: [
{
id: 'docs/guides',
name: 'guides',
children: [{id: 'docs/guides/getting-started.md', name: 'getting-started.md'}],
},
{
id: 'docs/a-very-long-file-name-that-does-not-fit-the-panel.md',
name: 'a-very-long-file-name-that-does-not-fit-the-panel.md',
},
],
},
{
id: 'src',
name: 'src',
children: [
{id: 'src/index.html', name: 'index.html'},
{id: 'src/styles.css', name: 'styles.css'},
],
},
{id: 'README.md', name: 'README.md'},
];
export default function FileTreeDemo() {
const [current, setCurrent] = useState('src/index.html');
const renderItem = useCallback(({node, isFolder}: FileTreeItemState) => {
const select = (event: MouseEvent) => {
event.preventDefault();
setCurrent(node.id);
};
return (
<FileTree.Item render={isFolder ? undefined : <a href={`#${node.id}`} onClick={select} />}>
<FileTree.ItemIcon />
<FileTree.ItemLabel>{node.name}</FileTree.ItemLabel>
</FileTree.Item>
);
}, []);
return (
<div style={{inlineSize: 260}}>
<FileTree.Root aria-label="Files" items={items} currentId={current}>
{renderItem}
</FileTree.Root>
</div>
);
} Usage
import {FileTree, type FileTreeItemState} from '@earendil-works/elanor-react';
const renderItem = ({node}: FileTreeItemState) => (
<FileTree.Item>
<FileTree.ItemIcon />
<FileTree.ItemLabel>{node.name}</FileTree.ItemLabel>
</FileTree.Item>
);
<FileTree.Root aria-label="Files" items={items}>
{renderItem}
</FileTree.Root>;
items is the nested data: each node has a unique id and a name, and a folder has children, even if empty. Keep the items reference stable between renders. Root calls its children once per visible row with the row’s node, level, isFolder, expanded, and current; wrap the function in useCallback, because rows re-render when it changes. Name the tree with aria-label.
ItemIcon shows a chevron and a folder icon on folders, and a file icon on files; pass an icon as its children to replace the folder or file icon. Each level indents by one icon width, with a guide line under the parent’s chevron.
Open folders
Clicking a folder opens or closes it, instantly; only its chevron turns, with semantic.transition.movement.disclosure. Pass defaultExpanded with the ids of folders open at first, or control them with expanded and onExpandedChange.
Code
import {
Button,
FileTree,
Stack,
type FileTreeItemState,
type FileTreeNode,
} from '@earendil-works/elanor-react';
import {useCallback, useState} from 'react';
const items: FileTreeNode[] = [
{
id: 'reports',
name: 'reports',
children: [
{
id: 'reports/2026',
name: '2026',
children: [
{id: 'reports/2026/q1.pdf', name: 'q1.pdf'},
{id: 'reports/2026/q2.pdf', name: 'q2.pdf'},
],
},
{id: 'reports/summary.md', name: 'summary.md'},
],
},
{
id: 'data',
name: 'data',
children: [{id: 'data/results.csv', name: 'results.csv'}],
},
];
const folderIds = ['reports', 'reports/2026', 'data'];
export default function FileTreeControlled() {
const [expanded, setExpanded] = useState<string[]>(['reports']);
const renderItem = useCallback(
({node}: FileTreeItemState) => (
<FileTree.Item>
<FileTree.ItemIcon />
<FileTree.ItemLabel>{node.name}</FileTree.ItemLabel>
</FileTree.Item>
),
[],
);
return (
<Stack gap="medium">
<Stack direction="row" gap="small">
<Button hierarchy="secondary" onClick={() => setExpanded(folderIds)}>
Expand all
</Button>
<Button hierarchy="secondary" onClick={() => setExpanded([])}>
Collapse all
</Button>
</Stack>
<div style={{inlineSize: 260}}>
<FileTree.Root
aria-label="Files"
items={items}
expanded={expanded}
onExpandedChange={setExpanded}
>
{renderItem}
</FileTree.Root>
</div>
</Stack>
);
} Links and the current file
Render a file’s Item as a link with render={<a href="…" />}, so it opens with a click, Enter, or the browser’s link menu. currentId marks that file with aria-current="page" and the semantic.color.background.fill.ghost.neutral.active fill. When it changes, the file’s folders open, without closing others, and the file scrolls into view. Without a controlled expanded, its folders are open from the first render, also on the server.
Names that don’t fit end with an ellipsis. Hovering or focusing the row with the keyboard shows the full name over it.
Keyboard
The tree is one Tab stop: the current file, or else the first row.
| Key | Action |
|---|---|
| Up, Down | Previous or next row |
| Home, End | First or last row |
| Right | Opens a folder, or moves to its first child when open |
| Left | Closes a folder, or moves to the parent |
| Enter | Opens or closes a folder, or follows a file’s link |
| Space | Opens or closes a folder |
| Letters | Next row whose name starts with the typed text |
| Escape | Hides the full name |
Right and Left swap in right-to-left layouts.
Guide line
Change the guide with --file-tree-guide on Root: an image one indent step wide (semantic.dimension.size.xSmall plus semantic.dimension.borderWidth.default) and one row tall, repeated once per level.
.files {
--file-tree-guide: url('./guide.svg');
}
Large trees
All visible rows are rendered, without virtualization. Moving between rows takes one frame at any size; first render takes about 45ms for 1,000 rows and 300ms for 10,000.
API
FileTree.Item
The row and treeitem. Pass render={<a href />} for files that navigate.
Renders <div>. Accepts its HTML attributes, ref, and render.
FileTree.ItemIcon
Chevron and folder icon for folders, file icon for files. Pass children to replace the icon.
Renders <span>. Accepts its HTML attributes and ref.
FileTree.ItemLabel
The name. Truncates with an ellipsis; indentation stays intact.
Renders <span>. Accepts its HTML attributes and ref.
FileTree.Root
| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | ReadonlyArray<Node> | - | Nested tree data. Keep the reference stable between renders. |
expanded | ReadonlyArray<string> | - | Ids of open folders (controlled). |
defaultExpanded | ReadonlyArray<string> | [] | Ids of folders open on first render (uncontrolled). |
onExpandedChange | ((expanded: string[]) => void) | - | Called with the ids of open folders when a folder opens or closes. |
currentId | string | null | null | Id of the current file. Its folders open when it changes, and it becomes the tree's Tab stop while focus is elsewhere. |
childrenrequired | (state: FileTreeItemState<Node>) => ReactNode | - | Renders one row. Memoize it (useCallback) for large trees: rows re-render when it changes. |
Renders <div>. Also accepts its HTML attributes and ref.