A zero-dependency, framework-agnostic file tree component written in TypeScript. Features drag-and-drop, context menus, keyboard navigation, theming, and RTL support.
For use in LiveCodes.
npm install @live-codes/file-treeimport { FileTree } from "@live-codes/file-tree";
import "@live-codes/file-tree/styles.css";
const tree = new FileTree("#container", {
data: [
{ path: "src/index.ts", type: "file" },
{ path: "src/utils/helpers.ts", type: "file" },
{ path: "src/utils/constants.ts", type: "file" },
{ path: "package.json", type: "file" },
{ path: "README.md", type: "file" },
],
selected: "src/index.ts",
theme: "dark",
direction: "ltr",
});
// Listen to events
tree.on("select", (e) => console.log("Selected:", e.path));
tree.on("rename", (e) => console.log("Renamed:", e.oldPath, "->", e.path));
tree.on("move", (e) => console.log("Moved:", e.oldPath, "->", e.path));
tree.on("delete", (e) => {
e.preventDefault();
const confirmed = confirm(`Delete "${e.path}"?`);
if (confirmed) tree.removeNode(e.path);
});
tree.on("change", (e) => console.log("Tree changed:", e.tree));Parent folders are automatically created from paths. In the example above, the src and src/utils folders are inferred from the file paths — you don't need to declare them.
You can also declare folders explicitly when you want empty folders or want to attach metadata:
const tree = new FileTree("#container", {
data: [
{ path: "src", type: "folder" },
{ path: "src/index.ts", type: "file" },
{ path: "dist", type: "folder" }, // empty folder
],
});new FileTree(container: HTMLElement | string, options?: FileTreeOptions)The container argument can be a CSS selector string or an HTMLElement.
interface FileTreeNodeData {
/** Full path (e.g. "src/utils/helpers.ts") — used as the unique identifier. */
path: string;
/** Whether this is a file or folder. */
type: "file" | "folder";
/** Custom SVG string to override the default icon. */
icon?: string;
/** Arbitrary user data. */
meta?: Record<string, unknown>;
}The createNode utility returns an array that includes the requested node plus all intermediate parent folders:
import { createNode } from "@live-codes/file-tree";
const nodes = createNode("src/components/Button.tsx", "file");
// Returns:
// [
// { path: 'src', type: 'folder' },
// { path: 'src/components', type: 'folder' },
// { path: 'src/components/Button.tsx', type: 'file' },
// ]Spread multiple createNode calls into your data array — duplicates are automatically deduplicated:
const tree = new FileTree("#container", {
data: [
...createNode("src/index.ts", "file"),
...createNode("src/utils.ts", "file"),
...createNode("package.json", "file"),
],
});| Option | Type | Default | Description |
|---|---|---|---|
data |
FileTreeNodeData[] |
[] |
Initial flat data array |
selected |
string |
'' |
Path of the initially selected node |
theme |
'light' | 'dark' |
'dark' |
Color theme |
direction |
'ltr' | 'rtl' |
'ltr' |
Text direction |
indent |
number |
16 |
Pixels per indentation level |
dragAndDrop |
boolean |
true |
Enable drag and drop |
toolbar |
ToolbarOptions | false |
See below | Toolbar configuration |
contextMenu |
ContextMenuOptions | false |
See below | Context menu configuration |
icons |
Record<string, string> |
{} |
Custom file extension → SVG icon map |
sort |
boolean | Comparator |
true |
Sort nodes (folders first, alphabetical) |
{
createFile?: boolean; // default: true
createFolder?: boolean; // default: true
expandAll?: boolean; // default: true
collapseAll?: boolean; // default: true
custom?: ToolbarButton[];
}{
createFile?: boolean; // default: true
createFolder?: boolean; // default: true
rename?: boolean; // default: true
delete?: boolean; // default: true
copy?: boolean; // default: false
custom?: ContextMenuItem[];
}interface ToolbarButton {
id: string;
label: string;
icon?: string; // SVG string
title?: string; // Tooltip
onClick: () => void;
}interface ContextMenuItem {
id: string;
label: string;
icon?: string;
shortcut?: string;
visible?: (node: FileTreeNodeData) => boolean;
onClick: (node: FileTreeNodeData) => void;
}| Method | Description |
|---|---|
expand(path) |
Expand a folder |
collapse(path) |
Collapse a folder |
expandAll() |
Expand all folders |
collapseAll() |
Collapse all folders |
select(path) |
Select a node |
| Method | Description |
|---|---|
addNode(node) |
Add a node (parent folders auto-created from path) |
removeNode(path) |
Remove a node and its descendants |
renameNode(path, newName) |
Rename a node (changes only the last path segment) |
moveNode(sourcePath, targetParentPath) |
Move a node to a new parent folder ('' or null for root) |
setData(data) |
Replace the entire tree |
getData() |
Get a clone of the flat data array |
getNode(path) |
Get a single node by path |
getSelectedNode() |
Get the currently selected node |
| Method | Description |
|---|---|
setTheme('light' | 'dark') |
Change the theme |
getTheme() |
Get current theme |
setDirection('ltr' | 'rtl') |
Change text direction |
getDirection() |
Get current direction |
| Method | Description |
|---|---|
destroy() |
Remove the tree and clean up all listeners |
tree.on(eventType, handler);
tree.off(eventType, handler);| Event | Fired when |
|---|---|
select |
A node is selected |
expand |
A folder is expanded |
collapse |
A folder is collapsed |
create |
A new node is created (after name is committed) |
rename |
A node is renamed |
delete |
A node is deleted |
move |
A node is moved via drag-and-drop or API |
drop |
External files are dropped into the tree |
change |
Any structural change to the tree data |
Every event handler receives a FileTreeEvent:
interface FileTreeEvent {
type: FileTreeEventType;
node: FileTreeNodeData; // The affected node
path: string; // Current path (same as node.path)
oldPath?: string; // Previous path (rename/move)
parentPath: string; // Parent folder path ('' for root)
parentNode: FileTreeNodeData | null;
tree: FileTreeNodeData[]; // Full flat data snapshot
data?: { files: FileList; items: DataTransferItemList }; // Drag-and-drop
}| Key | Action |
|---|---|
↑ / ↓ |
Navigate between visible nodes |
→ |
Expand folder or move to first child |
← |
Collapse folder or move to parent |
Enter / Space |
Toggle folder expand/collapse |
F2 |
Rename selected node |
Delete |
Delete selected node |
All visual properties are controlled by CSS custom properties. Override them on .ft-root or on theme-specific selectors:
.ft-root[data-theme="dark"] {
--ft-bg: #1a1b26;
--ft-color: #c0caf5;
--ft-node-hover: #292e42;
--ft-node-selected: #33467c;
--ft-drop-indicator: #7aa2f7;
/* ... see styles.css for all variables */
}Map the file tree variables to your app's existing CSS variables:
.ft-root[data-theme="dark"] {
--ft-bg: var(--layout);
--ft-color: var(--link);
--ft-node-hover: var(--darker-bg-active);
--ft-node-selected: var(--dark-bg-active);
--ft-toolbar-bg: var(--layout);
--ft-toolbar-border: var(--color30);
--ft-context-bg: var(--dropdown-bg-color);
--ft-context-border: var(--dark-bg-color);
--ft-context-color: var(--dropdown-color);
--ft-context-hover: var(--dropdown-bg-active);
--ft-input-bg: var(--input-bg-color);
--ft-input-color: var(--input-color);
--ft-input-border: var(--input-border-color);
--ft-border-radius: var(--rs);
}
.ft-root[data-theme="light"] {
--ft-bg: var(--layout);
--ft-color: var(--dark-color);
--ft-node-hover: var(--dark-bg-active);
--ft-node-selected: var(--color80);
--ft-toolbar-bg: var(--layout);
--ft-toolbar-border: var(--color80);
--ft-context-bg: var(--dropdown);
--ft-context-color: var(--dark-color);
}The library exports a few utility functions for working with paths:
import {
createNode, // Create node(s) with auto parent folders
normalizePath, // Normalize a path string
getName, // "src/index.ts" → "index.ts"
getParentPath, // "src/index.ts" → "src"
getExtension, // "index.ts" → "ts"
} from "@live-codes/file-tree";All modern browsers (Chrome, Firefox, Safari, Edge). Uses standard HTML5 Drag and Drop API and CSS custom properties.
MIT