Skip to content

FileTree

A file-system-flavored specialization of TreeView. It maps a file/folder model onto the tree, auto-assigns file-type icons by extension (plus open/closed folder glyphs), and adds drag-and-drop import of OS files onto a folder or the root. Expansion, selection, keyboard navigation, and sizing are inherited from TreeView unchanged.

Live Preview

Import

import { FileTree } from 'entangle-ui';
import type { FileTreeNode } from 'entangle-ui';

Usage

FileTree takes a nested array of FileTreeNodes. Each node has a kind of 'file' or 'folder'; folders carry children. File-type icons are derived from the file name (or an explicit ext), so you don’t assign them yourself.

const nodes: FileTreeNode[] = [
{
id: 'src',
name: 'src',
kind: 'folder',
children: [
{ id: 'btn', name: 'Button.tsx', kind: 'file' },
{ id: 'logo', name: 'logo.svg', kind: 'file' },
],
},
{ id: 'readme', name: 'README.md', kind: 'file' },
];
<FileTree nodes={nodes} defaultExpandedIds={['src']} />;

Expanding folders

Clicking anywhere on a folder row toggles it open/closed — not just the chevron (expandOnClick, on by default). Files are unaffected. Set expandOnClick={false} to require a chevron click instead.

// Whole-row toggle (default)
<FileTree nodes={nodes} />
// Require the chevron
<FileTree nodes={nodes} expandOnClick={false} />

Drag-and-drop import

Provide onImport to enable the import drop zone. Dropping OS files onto a folder targets that folder; dropping a file row targets its parent folder; dropping on empty space targets the root (targetFolder: null). The active target folder is highlighted while dragging. FileTree only reports intent — apply the change to your data and pass back fresh nodes.

Drag-and-drop import

<FileTree
nodes={nodes}
onImport={({ files, targetFolder }) => {
uploadInto(targetFolder?.id ?? 'root', files);
}}
/>

File-type icons

Icons are resolved from the file extension into a small set of buckets (image, media, code, archive, text) backed by the library icon set, with open/closed folder glyphs. Pass resolveIcon to override per node — return undefined to fall back to the built-in icon.

File-type icons

<FileTree
nodes={nodes}
resolveIcon={(node, { expanded }) =>
node.name.endsWith('.json') ? <StarIcon color="warning" /> : undefined
}
/>

The classifyExtension / getFileIconKind helpers used internally are also exported, in case you want to drive your own UI from the same mapping.

Coloring icons

Built-in icons follow the theme (folders use text.secondary, files text.muted), so a theme change re-colors them. For per-type or per-node colors, return a colored icon from resolveIcon — every library icon takes a color prop (a theme key like accent / success / warning, or any CSS color).

Colored icons by type

<FileTree
nodes={nodes}
resolveIcon={(node, { expanded }) => {
if (node.kind === 'folder') {
return expanded ? (
<FolderOpenIcon color="accent" />
) : (
<FolderIcon color="accent" />
);
}
if (node.name.endsWith('.png')) return <ImageIcon color="success" />;
return undefined; // fall back to the built-in icon
}}
/>

Selection

Selection reuses TreeView’s controlled/uncontrolled model verbatim — single (default), multiple (Ctrl / Shift click, Ctrl+A), or none.

Selection

<FileTree
nodes={nodes}
selectionMode="multiple"
selectedIds={selected}
onSelectionChange={setSelected}
/>

Sizes

Sizes

<FileTree nodes={nodes} size="sm" />
<FileTree nodes={nodes} size="md" />
<FileTree nodes={nodes} size="lg" />

Relationship to TreeView

FileTree renders a TreeView internally and feeds it derived nodes — it does not reimplement expansion, selection, or keyboard navigation. The same controlled/uncontrolled props (expandedIds / defaultExpandedIds, selectedIds / defaultSelectedIds) and the same keyboard behaviour apply. Reach for TreeView directly when you need a generic hierarchy; reach for FileTree when the data is files and folders.

The drag-and-drop import is built on top of TreeView’s generic drop-target props (dropTargetId, onNodeDragOver / onNodeDragLeave / onNodeDrop), which are available for building your own drop interactions on a plain TreeView as well.

Styling

FileTree is themed entirely through the --etui-* theme contract — no colors, spacings, or fonts are hard-coded. There are three ways to restyle it.

Render & content overrides

  • className / style are applied to the root container (style is also the place to set --etui-* overrides — they cascade to every row).
  • resolveIcon swaps a node’s icon; renderNode replaces a row’s whole inner content (icon + label); renderActions adds trailing controls.

Targeting hooks

The internal class names are compiled (hashed) by Vanilla Extract, so target the stable structural hooks instead:

SelectorMatches
[role="tree"]The tree region.
[role="treeitem"]Every row.
[role="treeitem"][aria-selected="true"]Selected rows.
[role="treeitem"][data-drop-target="true"]The folder currently highlighted as a drop target.
[data-root-active="true"]The container while files are dragged over the root.
#treenode-<id>A specific row by node id.

Theme tokens consumed

Override these on any ancestor to re-skin FileTree (or use createCustomTheme(...) for a whole-app palette):

TokenUsed for
--etui-color-accent-primarySelected-row tint and the active drop-target highlight (row + root).
--etui-color-surface-hoverRow hover background.
--etui-color-border-focusFocused-row ring.
--etui-color-border-defaultGuide lines.
--etui-color-text-primaryFile / folder names; chevron hover.
--etui-color-text-secondaryFolder icons.
--etui-color-text-mutedFile icons, chevrons, and empty-state text.
--etui-radius-smContainer corner rounding.
--etui-spacing-xs, --etui-spacing-mdIcon gap / row padding; empty-state padding.
--etui-font-size-md, --etui-font-size-lgSize-scaled label text.
--etui-line-height-normalLabel line height.
--etui-transition-fastChevron rotation and hover transitions.

Re-skinned via token overrides

<FileTree
nodes={nodes}
style={{
['--etui-color-accent-primary' as string]: '#d946ef',
['--etui-color-text-secondary' as string]: '#d946ef',
}}
/>

Internationalization

FileTree renders no copy of its own — file and folder names come from your data, and the file-type icons are decorative. The only built-in strings are the tree’s accessible name and the empty-state text, both overridable through the labels prop (a Partial<FileTreeLabels>, so anything you omit keeps its English default). An explicit aria-label / emptyContent still wins over the matching label.

Localized labels (Polish)

<FileTree
nodes={nodes}
labels={{ treeLabel: 'Drzewo plików', emptyLabel: 'Brak plików' }}
/>

The English defaults are exported as DEFAULT_FILE_TREE_LABELS (spread and tweak it when you only need to change one key).

KeyTypeDefault
treeLabelstring"File tree"
emptyLabelstring"No files"

Props

Prop Type Default Description
nodes FileTreeNode[] Tree data — array of root-level files/folders.
expandedIds string[] Expanded node IDs (controlled).
defaultExpandedIds string[] Default expanded node IDs (uncontrolled).
onExpandedChange (expandedIds: string[]) => void Fired when expanded nodes change.
selectedIds string[] Selected node IDs (controlled).
defaultSelectedIds string[] Default selected node IDs (uncontrolled).
selectionMode 'single' | 'multiple' | 'none' 'single' Selection mode (reused from TreeView).
onSelectionChange (selectedIds: string[]) => void Fired when selected nodes change.
size 'sm' | 'md' | 'lg' 'md' Row size.
indent number 16 Indentation per depth level in pixels.
showChevrons boolean true Whether to show expand/collapse chevrons for folders.
showGuideLines boolean false Whether to show connecting guide lines.
expandOnClick boolean true Toggle a folder open/closed when its whole row is clicked (not just the chevron).
maxHeight number | string Maximum height before the tree scrolls.
resolveIcon (node: FileTreeNode, state: { expanded: boolean }) => ReactNode Override a node icon. Return undefined to fall back to the built-in extension map.
renderNode (node: FileTreeNode, state: FileTreeNodeState) => ReactNode Fully replace a node's inner content (icon + label).
renderActions (node: FileTreeNode, state: FileTreeNodeState) => ReactNode Render trailing actions on the right of a row.
emptyContent ReactNode Content shown when nodes is empty.
onImport (payload: { files: File[]; targetFolder: FileTreeNode | null }) => void Fired when OS files are dropped onto a folder or the root. Presence enables the import drop zone.
onNodeClick (node: FileTreeNode, event: MouseEvent) => void Fired when a node is clicked.
onNodeDoubleClick (node: FileTreeNode, event: MouseEvent) => void Fired when a node is double-clicked.
onNodeContextMenu (node: FileTreeNode, event: MouseEvent) => void Fired when a node is right-clicked.
labels Partial<FileTreeLabels> Override built-in strings (tree accessible name, empty-state text). Merged onto the defaults.
aria-label string Accessible name for the tree. Wins over labels.treeLabel; lands on the role="tree" element.
className string Additional CSS class names (on the root container).
testId string Test identifier for automated testing.

FileTreeNode

PropertyTypeDescription
idstringStable unique id within the tree.
namestringDisplay name, e.g. "Button.tsx".
kind'file' | 'folder'File vs. folder. Folders accept dropped files.
extstringExtension override (no dot). Inferred from name.
pathstringOptional path (informational; surfaced unchanged).
childrenFileTreeNode[]Child entries (folders).
disabledbooleanDim + non-interactive.
dataRecord<string, unknown>Arbitrary consumer payload.

Accessibility

Accessibility is inherited from TreeView:

  • The container is role="tree"; each row is role="treeitem" with aria-expanded / aria-selected and aria-level.
  • aria-activedescendant tracks the focused node; focus is scrolled into view.
  • Full keyboard navigation (Arrow keys, Home/End, Enter, Space, Ctrl+A, Shift+Arrow range selection). File-type icons are decorative (aria-hidden), so the file name is the accessible label.