A modern, lightweight file tree component for the web, inspired by Geist UI File-Tree and its icons. Built as a custom element with TypeScript support.
- 🌳 Custom Element: Drop-in
<file-tree>component - 📁 Hierarchical Structure: Support for nested folders and files
- 🎯 Interactive: Click, right-click, and keyboard navigation
- 🔄 Async Support: Built-in loading states for dynamic content
- 📦 Zero Dependencies: Pure vanilla JavaScript
- 🔧 TypeScript Ready: Full type definitions included
import { Tree, Folder, File } from 'https://cdn.jsdelivr.net/npm/@webreflection/file-tree/prod.js';
const tree = new Tree;
// add an empty text file
tree.append(new File([], 'test.txt'));
// will provide details once an Item is clicked
tree.onclick = (event) => {};
// will provide details once an Item is right-clicked
tree.oncontextmenu = (event) => {};Each Tree implements the whole Folder interface plus a selected accessor that returns the last item that was selected on such tree and a query(path: string): Item[] | null utility to retrieve all items up to the target as a flat list or null, if no path is found.
As extra feature, Tree methods such as update, rename and remove allow passing a path instead of an item to simplify nested tree handling via strings.
When an item is clicked, or right-clicked, the optional click / contextmenu event handler will be dispatched via CustomEvent and its detail property:
action: "open" | "close" | "click"whereopenorcloseare folders only whileclickis for files.folder: booleanindicating if the item is a folder.originalTargetwhich is the<li>element representing the target File or Folderownerwhich is the parent folder needed to operate with the target (remove, rename, update).path: stringto retrieve the whole path in Web compat format, such as:some/path/file.txt.target: File | Folderwhich is the related file or folder reference that was clicked.
If the listener invokes event.preventDefault() the logic won't do anything else or, in the contextmenu case, it will prevent the default menu from appearing.
If the click listener invokes event.waitUntil(Promise<unknown>):void, needed to fetch the folder or file content asynchronously, as example, the UI will hint something is waiting to happen and it will finish once that promise has been resolved.
The abstract interface that can be directly or lazily expanded.
import { Tree, Folder, File } from 'https://cdn.jsdelivr.net/npm/@webreflection/file-tree/prod.js';
const tree = new Tree;
const assets = new Folder('assets');
const src = new Folder('src');
tree.append(assets, src);
src.append(new File([], 'main.js'));The main file tree component that extends HTMLElement and implements the Folder interface.
selected: Item | null- Returns the last selected itemitems: Item[]- All items in the tree (alias:files)
append(...items: (string | object | Item)[]): this- Add items to the treeremove(...items: Item[]): this- Remove items from the treerename(item: Item, name?: string): Item | Promise<Item>- Rename an itemupdate(file: File, content: Content | Content[]): File- Update file contentquery(path: string): Item[] | null- Get items by path
tree.addEventListener('click', (event) => {
const { action, folder, originalTarget, owner, path, target } = event.detail;
if (action === 'open' && folder) {
// Handle folder opening
event.waitUntil(loadFolderContent(target));
} else if (action === 'click' && !folder) {
// Handle file click
console.log('File clicked:', path);
}
});
tree.addEventListener('contextmenu', (event) => {
const { action, folder, path, target } = event.detail;
// Show custom context menu
showContextMenu(event, target);
event.preventDefault(); // Prevent default browser menu
});Represents a directory in the file tree.
name: string- Folder nametype: "folder"- Always returns "folder"size: number- Total size of all items in bytesitems: Item[]- All items in the folder (alias:files)
append(...items: (string | object | Item)[]): this- Add itemsremove(...items: Item[]): this- Remove itemsrename(item: Item, name?: string): Item | Promise<Item>- Rename an itemupdate(file: File, content: Content | Content[]): File- Update file content
Extends the native File class with additional utilities.
new File(content, name, options?)content: Content | Content[]- File contentname: string- File name (automatically sanitized)options?: { type?: string }- Optional type override
// Text file (type inferred from extension)
const readme = new File(['# My Project'], 'README.md');
// Binary file with explicit type
const image = new File([''], 'logo.png', { type: 'image/png' });
// Custom type
const config = new File(['{"key": "value"}'], 'config.json', {
type: 'application/json'
});When items are clicked or right-clicked, a CustomEvent is dispatched with the following detail properties:
| Property | Type | Description |
|---|---|---|
action |
"open" | "close" | "click" |
Action performed (open/close for folders, click for files) |
folder |
boolean |
Whether the item is a folder |
originalTarget |
HTMLLIElement |
The <li> element representing the item |
owner |
Folder |
Parent folder containing the item |
path |
string |
Full path in web-compatible format (e.g., src/components/Button.jsx) |
target |
File | Folder |
The actual file or folder object |
Use event.waitUntil() for asynchronous operations:
tree.addEventListener('click', (event) => {
const { action, folder, target } = event.detail;
if (action === 'open' && folder) {
// Load folder content asynchronously
event.waitUntil(
fetchFolderContent(target.name).then(content => {
content.forEach(item => target.append(item));
})
);
}
});tree.addEventListener('click', async (event) => {
const { action, folder, target } = event.detail;
if (action === 'open' && folder) {
event.waitUntil(
fetch(`/api/files/${target.name}`)
.then(response => response.json())
.then(files => {
files.forEach(file => {
if (file.type === 'folder') {
target.append(new Folder(file.name));
} else {
target.append(new File([file.content], file.name, {
type: file.mimeType
}));
}
});
})
);
}
});// Rename a file and get the new one
const renamedFile = tree.rename('src/main.js', 'app.js');
// Update file content and get the new file
const updatedFile = tree.update('src/app.js', ['console.log("Updated!")']);
// Remove a folder or file
tree.remove('src/components');<file-tree>
<ul>
<li class="file" data-bytes="1024">
<button>package.json</button>
</li>
<li class="folder">
<button>src</button>
<ul>
<li class="file" data-type="text/javascript">
<button>index.js</button>
</li>
<li class="folder opened">
<button>components</button>
<ul>
<li class="file">
<button>Button.jsx</button>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</file-tree>- Chrome 67+
- Firefox 63+
- Safari 11.1+
- Edge 79+
Geist UI Icons are under MIT license, here slightly modified for this project purpose.
Everything else is under MIT © Andrea Giammarchi.