Modern File Tree Web Component with Async Loading – file-tree

Category: Javascript , Recommended | August 27, 2025
AuthorWebReflection
Last UpdateAugust 27, 2025
LicenseMIT
Views155 views
Modern File Tree Web Component with Async Loading – file-tree

file-tree is a modern, lightweight folder/file tree component for the web that creates an interactive file hierarchy.

It’s a dependency-free custom element, so you can drop it into any project/framework or not and get a functional file tree.

It can be used to display nested files and folders, handle user interactions like clicks and right-clicks, and even load folder contents asynchronously.

Features:

  • Custom Element: A simple <file-tree> tag is all you need to get started.
  • Hierarchical Structure: Natively supports nested folders and files.
  • Interactive: Responds to clicks, right-clicks, and keyboard navigation out of the box.
  • Async Support: Has a built-in loading state for fetching folder contents on the fly.

Use Cases:

  • Web-Based Code Editors: You can use it to build the file explorer pane, similar to what you see in VS Code. The async loading is perfect for fetching directory contents from a remote server as the user expands folders.
  • Cloud Storage Browsers: It’s a solid choice for creating a UI to navigate a user’s files in a cloud service. You can dynamically load the contents of a folder when a user clicks to expand it.
  • Documentation Sites: For sites with deeply nested content, it can serve as a table of contents or a file-based navigation tree.
  • Content Management Systems (CMS): It’s well-suited for building an asset or media manager where users need to browse and organize uploaded files and folders.

Basic Usage:

1. Install the file-tree package via npm:

# NPM
$ npm install @webreflection/file-tree

2. Import it into your project and create a file tree programmatically:

import { Tree, Folder, File } from './prod.js';
// Create a new tree instance
const tree = new Tree;
// Add it to your page
document.body.appendChild(tree);
// Add some files and folders
const dist = new Folder('dist');
dist.append(new File([], 'prod.js'));
tree.append(dist, new File([], 'README.md'));

3. You can also initialize the tree directly from your HTML structure. Here’s a full example:

<file-tree>
  <ul>
    <li class="file" data-bytes="1000"><button>a_file.txt</button></li>
    <li class="file"><button>file.bin</button></li>
    <li class="folder">
      <button>folder</button>
      <ul>
        <li class="file"><button data-details="file.txt">file.txt</button></li>
        <li class="folder opened">
          <button>folder</button>
          <ul>
            <li class="file" data-type="text/plain"><button>file.txt</button></li>
            <li class="text"><button>file.txt</button></li>
            <li class="file"><button>file.svg</button></li>
            <li class="file"><button>file.png</button></li>
          </ul>
        </li>
        <li class="folder">
          <button>folder</button>
          <ul>
            <li class="file"><button>file.md</button></li>
            <li class="file"><button>file.txt</button></li>
            <li class="file"><button>file.txt</button></li>
            <li class="file"><button>file.txt</button></li>
            <li class="folder opened">
              <button>folder</button>
              <ul>
                <li class="file" data-type="text/plain"><button>file.txt</button></li>
                <li class="text"><button>file.txt</button></li>
                <li class="file"><button>file.txt</button></li>
                <li class="file"><button>file.txt</button></li>
              </ul>
            </li>
          </ul>
        </li>
      </ul>
    </li>
    <li class="file"><button>file.txt</button></li>
    <li class="file"><button>file.txt</button></li>
  </ul>
</file-tree>
<script type="module">
  import { Tree, File, Folder } from './prod.js';
  document.querySelector('file-tree').addEventListener('click', event => {
    const { action, folder, owner, path, target } = event.detail;
    console.log({ action, folder, owner, path, target });
  });
  const tree = new Tree;
  document.body.appendChild(tree);
  const a = new Folder('a');
  const b = new Folder('b');
  a.append('b.txt');
  b.append('b.txt', 'a.txt');
  const c = new File([], 'c.txt', { type: 'text/plain' });
  globalThis.a = a;
  globalThis.b = b;
  globalThis.c = c;
  globalThis.tree = tree;
  tree.append(a, c, b, { name: 'n.txt' });
  tree.addEventListener('contextmenu', event => {
    const { action, folder, path, target } = event.detail;
    console.log({ action, folder, path, target });
    event.preventDefault();
  });
  tree.addEventListener('click', event => {
    const { action, folder, path, target } = event.detail;
    if (folder) {
      if (action === 'open') {
        event.waitUntil(new Promise(resolve => setTimeout(resolve, Math.random() * 1000)));
      }
    }
    else {
      console.log(path);
    }
  });
  // setTimeout(() => tree.rename(c), 1000);
</script>

Tree Component

The Tree class is the main component you’ll interact with. It extends HTMLElement, so you can use it as <file-tree> in your HTML or instantiate it with new Tree(). It also implements the full Folder interface, meaning you can add items directly to the root of the tree.

Properties

  • selected: Item | null: A read-only property that returns the last selected File or Folder instance, or null if nothing is selected.
  • items: Item[]: An array containing all the top-level File and Folder instances within the tree. This is an alias for the files property.

Methods

  • append(...items: (string | object | Item)[]): this: Adds one or more items to the root of the tree. You can pass File or Folder instances, plain objects like { name: 'new-file.txt' }, or just a string for a simple file.
  • remove(...items: (Item | string)[]): this: Removes one or more items from the tree. You can pass the actual Item instance or a string path like 'src/components'.
  • rename(item: Item | string, name?: string): Item | Promise<Item>: Renames a given item. If name is not provided, it will prompt the user for a new name. You can pass the Item instance or its path.
  • update(file: File | string, content: Content | Content[]): File: Updates the content of a specified file. You can identify the file by its instance or by its string path.
  • query(path: string): Item[] | null: A utility method to retrieve all items in a given path. It returns a flat array of Item instances leading to the target, or null if the path is not found. For example, tree.query('src/main.js') would return [srcFolder, mainJsFile].

Events

The Tree component dispatches click and contextmenu events. You can listen for them using tree.addEventListener(). The event’s detail object contains a rich payload:

PropertyTypeDescription
action"open" | "close" | "click"The specific action performed (open/close for folders, click for files).
folderbooleantrue if the item is a folder, false otherwise.
originalTargetHTMLLIElementThe actual <li> DOM element that was clicked.
ownerFolderThe parent folder that contains the item.
pathstringThe full, web-compatible path to the item (e.g., src/components/Button.jsx).
targetFile | FolderThe File or Folder instance that the event concerns.

Folder Component

The Folder class represents a directory within the file tree. It’s the primary way to organize items hierarchically.

Properties

  • name: string: The name of the folder.
  • type: "folder": A read-only property that always returns "folder".
  • size: number: The total size in bytes of all files contained within the folder, calculated recursively.
  • items: Item[]: An array of all File and Folder instances directly inside this folder. This is an alias for files.

Methods

The Folder class shares most of its methods with Tree for a consistent API:

  • append(...items: (string | object | Item)[]): this: Adds items inside this folder.
  • remove(...items: Item[]): this: Removes items from this folder.
  • rename(item: Item, name?: string): Item | Promise<Item>: Renames a direct child item.
  • update(file: File, content: Content | Content[]): File: Updates the content of a direct child file.

File Component

The File class extends the browser’s native File object, adding some convenience. It represents a single file in the tree.

Constructor

new File(content, name, options?)
  • content: Content | Content[]: The file’s content. This can be a string, a Blob, or an ArrayBuffer.
  • name: string: The name of the file. The library automatically sanitizes the name to remove invalid characters.
  • options?: { type?: string }: An optional object to override the MIME type. If not provided, the type is inferred from the file extension.

Example

// A simple text file. The type 'text/markdown' is inferred.
const readme = new File(['# My Project'], 'README.md');
// A binary file where we explicitly set the type.
const image = new File(['...binary data...'], 'logo.png', { type: 'image/png' });
// A file with a custom type.
const config = new File(['{"key": "value"}'], 'config.json', {
  type: 'application/json'
});

Async Operations with `event.waitUntil()`

When a user clicks to open a folder, you can fetch its contents from a server. The event.waitUntil() method is designed for this. You pass it a Promise, and the UI will automatically display a loading indicator on the folder until the promise resolves.

tree.addEventListener('click', (event) => {
  const { action, folder, target } = event.detail;
  if (action === 'open' && folder) {
    // If the folder is empty, fetch its content
    if (target.items.length === 0) {
      event.waitUntil(
        fetchFolderContent(target.name).then(content => {
          // Assuming content is an array of File and Folder instances
          target.append(...content);
        })
      );
    }
  }
});

FAQs

Q: Can I customize the visual appearance of the file tree?
A: Yes, the component uses standard CSS classes that you can style. The library generates semantic HTML with .file, .folder, and .opened classes. You can override these styles or add custom CSS to match your design requirements.

Q: How do I handle large directory structures without performance issues?
A: Use the async loading pattern with event.waitUntil() to load folder contents on demand. This prevents initial rendering bottlenecks and provides better user experience. Load only the immediate children when folders are expanded rather than the entire tree structure upfront.

Q: Can I use this with React or Vue?
A: Yes. It’s a standard Custom Element, so it works in any framework that supports them. You can use it in your JSX or Vue templates just like any other HTML tag. You’ll just need to handle event listeners according to your framework’s syntax.

Q: How do I handle right-click to show a custom context menu?
A: You listen for the contextmenu event. In your event handler, you can display your own menu and then call event.preventDefault() to stop the browser’s default context menu from appearing. The event.detail object will give you the target file or folder that was right-clicked.

Alternatives

You Might Be Interested In:


Leave a Reply