Skip to main content

Tree View

Tree views display hierarchical data in VS Code’s sidebar, panel areas, or other view containers. They’re perfect for file explorers, project structures, test runners, and any nested data visualization.

Overview

A tree view consists of two main components:
  • TreeDataProvider - Provides the data and structure
  • TreeView - Controls the view’s visibility and behavior

When to Use Tree Views

  • File and folder navigation
  • Project dependency visualization
  • Test suite hierarchies
  • Configuration management
  • Custom explorers for domain-specific data

Tree Data Provider

The TreeDataProvider interface defines how your data is structured and retrieved.

Interface

interface TreeDataProvider<T> {
  // Required: Get tree item representation
  getTreeItem(element: T): TreeItem | Thenable<TreeItem>;
  
  // Required: Get children of element (or root)
  getChildren(element?: T): ProviderResult<T[]>;
  
  // Optional: Get parent for reveal API
  getParent?(element: T): ProviderResult<T>;
  
  // Optional: Notify view of changes
  onDidChangeTreeData?: Event<T | T[] | undefined | null | void>;
  
  // Optional: Resolve tree item details
  resolveTreeItem?(item: TreeItem, element: T, token: CancellationToken): ProviderResult<TreeItem>;
}

Basic Implementation

1

Define your data model

Create classes representing your tree structure:
import * as vscode from 'vscode';

class FileNode {
  constructor(
    public readonly label: string,
    public readonly type: 'file' | 'folder',
    public readonly children?: FileNode[]
  ) {}
}
2

Implement TreeDataProvider

Create your data provider:
class FileTreeProvider implements vscode.TreeDataProvider<FileNode> {
  private _onDidChangeTreeData = new vscode.EventEmitter<FileNode | undefined | null | void>();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  constructor(private workspaceRoot: string) {}

  refresh(): void {
    this._onDidChangeTreeData.fire();
  }

  getTreeItem(element: FileNode): vscode.TreeItem {
    const treeItem = new vscode.TreeItem(
      element.label,
      element.type === 'folder'
        ? vscode.TreeItemCollapsibleState.Collapsed
        : vscode.TreeItemCollapsibleState.None
    );

    // Set icon
    treeItem.iconPath = element.type === 'folder'
      ? vscode.ThemeIcon.Folder
      : vscode.ThemeIcon.File;

    // Set context value for commands
    treeItem.contextValue = element.type;

    // Set command when clicked
    if (element.type === 'file') {
      treeItem.command = {
        command: 'myExtension.openFile',
        title: 'Open File',
        arguments: [element]
      };
    }

    return treeItem;
  }

  getChildren(element?: FileNode): Thenable<FileNode[]> {
    if (!this.workspaceRoot) {
      return Promise.resolve([]);
    }

    if (element) {
      // Return children of element
      return Promise.resolve(element.children || []);
    } else {
      // Return root elements
      return Promise.resolve(this.getRootNodes());
    }
  }

  private getRootNodes(): FileNode[] {
    return [
      new FileNode('src', 'folder', [
        new FileNode('index.ts', 'file'),
        new FileNode('utils.ts', 'file')
      ]),
      new FileNode('README.md', 'file')
    ];
  }
}
3

Register the tree view

Register in package.json and activate:
{
  "contributes": {
    "views": {
      "explorer": [
        {
          "id": "myExtension.fileExplorer",
          "name": "My File Explorer"
        }
      ]
    },
    "commands": [
      {
        "command": "myExtension.refreshExplorer",
        "title": "Refresh",
        "icon": "$(refresh)"
      }
    ],
    "menus": {
      "view/title": [
        {
          "command": "myExtension.refreshExplorer",
          "when": "view == myExtension.fileExplorer",
          "group": "navigation"
        }
      ]
    }
  }
}
export function activate(context: vscode.ExtensionContext) {
  const workspaceRoot = vscode.workspace.workspaceFolders?.[0].uri.fsPath || '';
  const treeDataProvider = new FileTreeProvider(workspaceRoot);
  
  const treeView = vscode.window.createTreeView('myExtension.fileExplorer', {
    treeDataProvider,
    showCollapseAll: true
  });

  context.subscriptions.push(
    treeView,
    vscode.commands.registerCommand('myExtension.refreshExplorer', () => {
      treeDataProvider.refresh();
    }),
    vscode.commands.registerCommand('myExtension.openFile', (node: FileNode) => {
      vscode.window.showInformationMessage(`Opening ${node.label}`);
    })
  );
}

TreeItem Configuration

The TreeItem class provides extensive customization options:

Properties

class TreeItem {
  // Display
  label?: string | TreeItemLabel;
  description?: string | boolean;
  tooltip?: string | MarkdownString;
  
  // Icon
  iconPath?: string | Uri | { light: Uri; dark: Uri } | ThemeIcon;
  
  // Behavior
  collapsibleState?: TreeItemCollapsibleState;
  command?: Command;
  contextValue?: string;
  
  // Advanced
  resourceUri?: Uri;
  id?: string;
  checkboxState?: TreeItemCheckboxState;
  accessibilityInformation?: AccessibilityInformation;
}

Complete Example

getTreeItem(element: FileNode): vscode.TreeItem {
  const treeItem = new vscode.TreeItem(
    element.label,
    element.children
      ? vscode.TreeItemCollapsibleState.Collapsed
      : vscode.TreeItemCollapsibleState.None
  );

  // Custom label with highlights
  treeItem.label = {
    label: element.label,
    highlights: [[0, 2]] // Highlight first 2 characters
  };

  // Description (shown in gray)
  treeItem.description = element.type === 'file' ? '100 lines' : `${element.children?.length} items`;

  // Rich tooltip
  const tooltip = new vscode.MarkdownString();
  tooltip.appendMarkdown(`**${element.label}**\n\n`);
  tooltip.appendMarkdown(`Type: ${element.type}`);
  treeItem.tooltip = tooltip;

  // Custom icon
  if (element.type === 'file') {
    treeItem.iconPath = new vscode.ThemeIcon(
      'file',
      new vscode.ThemeColor('charts.blue')
    );
  } else {
    treeItem.iconPath = {
      light: vscode.Uri.joinPath(this.extensionUri, 'resources', 'light', 'folder.svg'),
      dark: vscode.Uri.joinPath(this.extensionUri, 'resources', 'dark', 'folder.svg')
    };
  }

  // Context value for menu contributions
  treeItem.contextValue = `${element.type}-node`;

  // Resource URI for built-in commands
  if (element.type === 'file') {
    treeItem.resourceUri = vscode.Uri.file(element.filePath);
  }

  // Unique ID for state preservation
  treeItem.id = `${element.type}-${element.label}`;

  // Command on click
  treeItem.command = {
    command: 'myExtension.itemClicked',
    title: 'Open',
    arguments: [element]
  };

  return treeItem;
}

Advanced Features

Dynamic Updates

Notify the view when data changes:
class DynamicTreeProvider implements vscode.TreeDataProvider<FileNode> {
  private _onDidChangeTreeData = new vscode.EventEmitter<FileNode | undefined>();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  refresh(node?: FileNode): void {
    // Refresh specific node
    this._onDidChangeTreeData.fire(node);
  }

  refreshAll(): void {
    // Refresh entire tree
    this._onDidChangeTreeData.fire(undefined);
  }

  // Watch for file system changes
  constructor() {
    const watcher = vscode.workspace.createFileSystemWatcher('**/*');
    watcher.onDidCreate(() => this.refreshAll());
    watcher.onDidChange(() => this.refreshAll());
    watcher.onDidDelete(() => this.refreshAll());
  }
}

Revealing Elements

Programmatically reveal and select items:
const treeView = vscode.window.createTreeView('myView', {
  treeDataProvider
});

// Reveal and select element
await treeView.reveal(element, {
  select: true,
  focus: true,
  expand: true // Or number for levels: expand: 3
});
To use reveal(), you must implement the getParent() method in your TreeDataProvider.

Implementing getParent

class HierarchicalProvider implements vscode.TreeDataProvider<FileNode> {
  private parentMap = new Map<FileNode, FileNode>();

  getChildren(element?: FileNode): FileNode[] {
    const children = /* ... get children ... */;
    // Track parent relationships
    children.forEach(child => this.parentMap.set(child, element!));
    return children;
  }

  getParent(element: FileNode): FileNode | undefined {
    return this.parentMap.get(element);
  }

  // ... other methods
}

Checkboxes

Add checkboxes to tree items:
getTreeItem(element: FileNode): vscode.TreeItem {
  const treeItem = new vscode.TreeItem(element.label);
  
  // Simple checkbox
  treeItem.checkboxState = element.checked
    ? vscode.TreeItemCheckboxState.Checked
    : vscode.TreeItemCheckboxState.Unchecked;
  
  // Or with tooltip and accessibility
  treeItem.checkboxState = {
    state: vscode.TreeItemCheckboxState.Checked,
    tooltip: 'Select for deployment',
    accessibilityInformation: {
      label: 'Deployment checkbox',
      role: 'checkbox'
    }
  };
  
  return treeItem;
}

// Handle checkbox changes (register in package.json)
vscode.commands.registerCommand('myExtension.checkboxChanged', (items: FileNode[]) => {
  items.forEach(item => {
    item.checked = !item.checked;
  });
  treeDataProvider.refresh();
});

Context Menus

Add context menu items to tree nodes:

Define in package.json

{
  "contributes": {
    "menus": {
      "view/item/context": [
        {
          "command": "myExtension.deleteFile",
          "when": "view == myExtension.fileExplorer && viewItem == file-node",
          "group": "inline"
        },
        {
          "command": "myExtension.renameFile",
          "when": "view == myExtension.fileExplorer && viewItem == file-node"
        },
        {
          "command": "myExtension.addFile",
          "when": "view == myExtension.fileExplorer && viewItem == folder-node"
        }
      ]
    },
    "commands": [
      {
        "command": "myExtension.deleteFile",
        "title": "Delete",
        "icon": "$(trash)"
      },
      {
        "command": "myExtension.renameFile",
        "title": "Rename"
      },
      {
        "command": "myExtension.addFile",
        "title": "Add File"
      }
    ]
  }
}

Register Commands

context.subscriptions.push(
  vscode.commands.registerCommand('myExtension.deleteFile', (node: FileNode) => {
    vscode.window.showInformationMessage(`Deleting ${node.label}`);
    // Delete file and refresh tree
    treeDataProvider.refresh();
  }),
  vscode.commands.registerCommand('myExtension.renameFile', async (node: FileNode) => {
    const newName = await vscode.window.showInputBox({
      prompt: 'Enter new name',
      value: node.label
    });
    if (newName) {
      node.label = newName;
      treeDataProvider.refresh(node);
    }
  })
);

Best Practices

Performance

// ❌ Bad: Loading all data upfront
class SlowProvider implements vscode.TreeDataProvider<FileNode> {
  private allData = this.loadAllData(); // Expensive!
  
  getChildren(element?: FileNode) {
    return this.allData.filter(item => item.parent === element);
  }
}

// ✅ Good: Lazy loading
class FastProvider implements vscode.TreeDataProvider<FileNode> {
  async getChildren(element?: FileNode): Promise<FileNode[]> {
    // Only load children when needed
    if (element) {
      return this.loadChildrenOf(element);
    }
    return this.loadRootNodes();
  }
}

State Management

class StatefulProvider implements vscode.TreeDataProvider<FileNode> {
  private cache = new Map<string, FileNode[]>();

  async getChildren(element?: FileNode): Promise<FileNode[]> {
    const key = element?.id || 'root';
    
    // Check cache
    if (this.cache.has(key)) {
      return this.cache.get(key)!;
    }
    
    // Load and cache
    const children = await this.loadChildren(element);
    this.cache.set(key, children);
    return children;
  }

  refresh(node?: FileNode): void {
    // Clear cache for node
    if (node) {
      this.cache.delete(node.id);
    } else {
      this.cache.clear();
    }
    this._onDidChangeTreeData.fire(node);
  }
}

Common Patterns

Drag and Drop

class DragDropProvider implements vscode.TreeDataProvider<FileNode>, vscode.TreeDragAndDropController<FileNode> {
  dropMimeTypes = ['application/vnd.code.tree.myView'];
  dragMimeTypes = ['text/uri-list'];

  public async handleDrag(
    source: FileNode[],
    dataTransfer: vscode.DataTransfer,
    token: vscode.CancellationToken
  ): Promise<void> {
    dataTransfer.set(
      'application/vnd.code.tree.myView',
      new vscode.DataTransferItem(source)
    );
  }

  public async handleDrop(
    target: FileNode | undefined,
    dataTransfer: vscode.DataTransfer,
    token: vscode.CancellationToken
  ): Promise<void> {
    const item = dataTransfer.get('application/vnd.code.tree.myView');
    if (item) {
      const nodes = item.value as FileNode[];
      // Handle the drop
      this.moveNodes(nodes, target);
    }
  }
}

// Register with drag and drop
vscode.window.createTreeView('myView', {
  treeDataProvider: provider,
  dragAndDropController: provider
});
Drag and drop must be declared in package.json with "enableDragAndDrop": true in the view definition.

Resources

Tree View Guide

Complete tree view documentation

Tree View Sample

Working example code

Build docs developers (and LLMs) love