npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aiquants/directory-tree

v3.11.0

Published

High-performance directory tree component for React with virtual scrolling and file selection

Readme

@aiquants/directory-tree

A high-performance React directory tree component with virtualization, file selection, and theming support.

Features

  • 🚀 High Performance: Built with @aiquants/virtualscroll to handle large directory structures efficiently with O(log n) operations
  • 🌀 Ultrafast Scrolling: Inherits adaptive tap scroll circle from VirtualScroll for navigating massive datasets
  • 📁 File Selection: Interactive file selection with visual feedback and multiple selection modes
  • 🎨 Theming: Customizable line colors with external theme control support
  • ♿ Accessibility: Full keyboard navigation and screen reader support
  • 📱 Responsive: Optimized for both desktop and mobile interfaces
  • 🔧 TypeScript: Complete TypeScript support with comprehensive type definitions
  • 💾 State Persistence: Automatic localStorage persistence for expansion states
  • 🎯 Flexible Selection: Support for none, single, or multiple selection modes

Installation

npm install @aiquants/directory-tree
# or
yarn add @aiquants/directory-tree
# or
pnpm add @aiquants/directory-tree

Peer Dependencies

This package requires the following peer dependencies:

npm install react react-dom @aiquants/virtualscroll

Quick Start

import React from 'react';
import { DirectoryTree, useDirectoryTreeState } from '@aiquants/directory-tree';
import { useTheme } from './hooks/useTheme'; // Your theme hook
import type { DirectoryEntry } from '@aiquants/directory-tree';

const sampleData: DirectoryEntry[] = [
  {
    name: 'src',
    absolutePath: '/src',
    relativePath: 'src',
    children: [
      {
        name: 'components',
        absolutePath: '/src/components',
        relativePath: 'src/components',
        children: [
          {
            name: 'App.tsx',
            absolutePath: '/src/components/App.tsx',
            relativePath: 'src/components/App.tsx',
            children: null
          }
        ]
      }
    ]
  }
];

export default function App() {
  const { theme } = useTheme();
  const {
    toggle,
    isExpanded,
    expandMultiple,
    collapseMultiple,
    isPending
  } = useDirectoryTreeState({
    storageKey: 'my-directory-tree'
  });

  // Calculate line color based on theme
  const lineColor = theme === "dark" ? "#4A5568" : "#A0AEC0";

  const handleEntryClick = (event: DirectoryTreeClickEvent) => {
    console.log(`Entry clicked: ${event.entry.absolutePath}`);
  };

  return (
    <div className="h-96 w-full border rounded-lg">
      <DirectoryTree
        entries={sampleData}
        expansion={{
          toggle,
          isExpanded,
          expandMultiple,
          collapseMultiple,
          isPending
        }}
        selection={{
          onEntryClick: handleEntryClick,
          selectedPath: null
        }}
        visual={{
          lineColor,
          className: "h-full"
        }}
      />
    </div>
  );
}

API Reference

DirectoryTree Component

The main component for rendering the directory tree.

Props

| Prop | Type | Required | Description | | --- | --- | --- | --- | | entries | DirectoryEntry[] | Yes | Array of root directory entries to display | | expansion | object | Yes | Configuration for tree expansion state and behavior | | selection | object | Yes | Configuration for item selection | | visual | object | No | Visual customization options | | virtualScroll | DirectoryTreeVirtualScrollOptions | No | Pass-through options for the underlying VirtualScroll component |

Expansion Options (expansion)

| Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | toggle | (path: string) => void | Yes | - | Function to toggle directory expansion state | | isExpanded | (path: string) => boolean | Yes | - | Function to check if a directory is expanded | | expandMultiple | (paths: string[]) => void | Yes | - | Function to expand multiple directories | | collapseMultiple | (paths: string[]) => void | Yes | - | Function to collapse multiple directories | | isPending | boolean | No | false | Whether the tree is in a pending state | | alwaysExpanded | boolean | No | false | If true, all directories are always expanded | | doubleClickAction | 'recursive' \| 'toggle' | No | 'recursive' | Action on double-clicking a directory |

Selection Options (selection)

| Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | onEntryClick | (event: DirectoryTreeClickEvent) => void | Yes | - | Callback function triggered when an entry is clicked | | selectedPath | string \| null | No | - | The currently selected file path | | mode | 'none' \| 'single' \| 'multiple' | No | 'none' | Selection mode for items | | selectedItems | Set<string> | No | - | Set of paths for currently selected items | | onSelectionChange | (entry: DirectoryEntry, isSelected: boolean) => void | No | - | Callback when item selection changes |

Visual Options (visual)

| Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | className | string | No | - | Optional CSS class name for the container | | style | React.CSSProperties | No | - | Optional inline styles for the container | | lineColor | string | No | '#A0AEC0' | The color of the tree lines | | showTreeLines | boolean | No | true | Flag indicating whether to render tree connector lines | | showExpandIcons | boolean | No | true | Flag indicating whether to render directory expand icons | | showDirectoryIcons | boolean | No | true | Flag indicating whether to render directory type icons | | showFileIcons | boolean | No | true | Flag indicating whether to render file type icons | | iconOverrides | DirectoryTreeIconOverrides | No | - | Icon overrides applied globally | | expandIconSize | number | No | - | Size of the expand icon | | itemHeight | number \| ((entry, index) => number) | No | 20 | Row height in px, or a function computing the height per entry (variable-height rows). Tree connector lines follow each row's cumulative offset. Invalid values (NaN / Infinity / 0 / negative) fall back to 20. Memoize the function to keep its identity stable. | | removeRootIndent | boolean | No | false | If true, removes the indentation and connector lines for root-level items | | highlightStyles | HighlightStyles | No | - | Highlight styles configuration for hover and selection states | | entryClassName | string | No | - | Additional CSS classes for each entry row | | entryStyle | React.CSSProperties | No | - | Additional inline styles for each entry row | | nameClassName | string | No | - | Additional CSS classes for the name label | | nameStyle | React.CSSProperties | No | - | Additional inline styles for the name label | | directoryNameClassName | string | No | - | Additional CSS classes specifically for directory names | | directoryNameStyle | React.CSSProperties | No | - | Additional inline styles specifically for directory names | | fileNameClassName | string | No | - | Additional CSS classes specifically for file names | | fileNameStyle | React.CSSProperties | No | - | Additional inline styles specifically for file names |

Virtual Scroll Options

virtualScroll lets you customize the embedded @aiquants/virtualscroll instance without re-implementing list rendering. Every option is optional and mirrors the VirtualScroll API.

  • overscanCount: Adjust how many items render beyond the viewport for smoother scrolling (default: 10).
  • scrollBarOptions: Configure scrollbar appearance and behavior (width, thumb drag, track click, arrow buttons, tap scroll circle).
  • behaviorOptions: Configure scrolling behavior (pointer drag, keyboard navigation, inertia, wheel speed).
  • onScroll, onRangeChange, className, background, initialScrollIndex, initialScrollOffset: Hook into scroll lifecycle or provide bespoke styling.

Example:

<DirectoryTree
  {...commonProps}
  virtualScroll={{
    overscanCount: 6,
    behaviorOptions: {
      enablePointerDrag: false,
    },
    scrollBarOptions: {
      width: 14,
      tapScrollCircleOptions: {
        radius: 32
      }
    }
  }}
/>;

useDirectoryTreeState Hook

A hook for managing directory tree state with localStorage persistence.

Parameters

| Parameter | Type | Description | | --- | --- | --- | | options | UseDirectoryTreeStateProps | Configuration options |

Options

| Option | Type | Description | | --- | --- | --- | | initialExpanded | Set<string> | Initially expanded directories | | storageKey | string | localStorage key for persistence (default: 'directory-tree-state') |

Returns

| Property | Type | Description | | --- | --- | --- | | expanded | Set<string> | Currently expanded directories | | toggle | (path: string) => void | Toggle directory expansion | | isExpanded | (path: string) => boolean | Check if directory is expanded | | expand | (path: string) => void | Expand a directory | | collapse | (path: string) => void | Collapse a directory | | expandMultiple | (paths: string[]) => void | Expand multiple directories | | collapseMultiple | (paths: string[]) => void | Collapse multiple directories | | collapseAll | () => void | Collapse all directories | | isPending | boolean | Whether a transition is pending |

DirectoryEntry Type

type DirectoryEntry = {
  name: string;
  absolutePath: string;
  relativePath: string;
  children: DirectoryEntry[] | null;
};

Styling

The component's hand-written classes ship in two CSS artifacts; choose by host type. The tree is built on @aiquants/virtualscroll, so that package's stylesheet is a peer CSS dependency — import it once alongside directory-tree. Theme control (light/dark) stays with the calling app.

  • Tailwind v4 host — import the components-only build in layer(components), do the same for the virtualscroll peer build, and let your Tailwind build generate the JSX utilities from the package source:

    /* app tailwind.css */
    @import "@aiquants/virtualscroll/styles/virtualscroll.css" layer(components);
    @import "@aiquants/directory-tree/styles/directory-tree.css" layer(components);
    @source "../node_modules/@aiquants/directory-tree/src/**/*.{ts,tsx}";
    /* monorepo: @source "../../../../packages/directory-tree/src/**/*.{ts,tsx}"; */

    directory-tree.css (Artifact A) carries only the hand-written .dt-* classes — no :root theme variables, no Tailwind utilities, no preflight — and it does not embed the virtualscroll stylesheet (the host imports virtualscroll's own Artifact A once, avoiding a duplicated, possibly stale copy). Do not also @import the standalone build here — the duplicated utilities flip the base/variant cascade order.

  • Non-Tailwind host — import the single self-contained standalone build; it bundles the .dt-* classes, every JSX utility, and the virtualscroll peer CSS for convenience:

    @import "@aiquants/directory-tree/styles/directory-tree.standalone.css";

    Dark styles key off the .dark class on <html>.

Theme Control

The lineColor prop allows you to control the tree line color based on your application's theme:

import { useTheme } from './hooks/useTheme';

function MyComponent() {
  const { theme } = useTheme();

  // Calculate line color based on theme
  const lineColor = theme === "dark" ? "#4A5568" : "#A0AEC0";

  return (
    <DirectoryTree
      // ... other props
      visual={{
        lineColor: lineColor
      }}
    />
  );
}

Custom Styling

You can customize the appearance by passing custom classes or inline styles for specific elements like entry rows, name labels, directory names, or file names, avoiding arbitrary CSS selectors overrides:

<DirectoryTree
  visual={{
    className: "custom-directory-tree",
    style: { height: '400px' },
    
    // Style the entry rows (containers)
    entryClassName: "rounded-md px-2 py-1 hover:bg-slate-100/50",
    entryStyle: { transition: "background-color 0.2s" },

    // Style the label text
    nameClassName: "text-sm font-medium",
    
    // Target directory or file names specifically
    directoryNameClassName: "text-slate-800 dark:text-slate-100",
    fileNameClassName: "text-slate-600 dark:text-slate-300",
  }}
  // ... other props
/>

Alternatively, you can customize components inside the container using global CSS classes:

.custom-directory-tree .directory-tree-entry {
  /* Custom styles for entry rows */
}

Alternatively, you can dynamically configure specific highlight styles for hover, directory selection, and item (file) selection by passing custom class names or inline styles via highlightStyles:

<DirectoryTree
  visual={{
    highlightStyles: {
      // Custom hover style
      hoverClassName: "bg-amber-100 dark:bg-amber-900/20",
      hoverStyle: { borderRight: "2px solid orange" },
      
      // Custom directory selection style
      directorySelectedClassName: "bg-emerald-100 dark:bg-emerald-900/20 font-bold",
      directorySelectedStyle: { borderLeft: "3px solid green" },
      
      // Custom item (file) selection style
      itemSelectedClassName: "bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800",
      itemSelectedStyle: { borderLeft: "3px solid indigo" },
    }
  }}
  // ... other props
/>

Advanced Usage

Row Height (fixed & variable)

Rows are 20px tall by default. Pass a number for a uniform height, or a function for variable-height rows — the virtual scroller and the tree connector lines both follow each row's resolved height. Memoize the function so its identity stays stable.

import { useCallback } from 'react';
import { DirectoryTree, type DirectoryEntry } from '@aiquants/directory-tree';

// Fixed height
<DirectoryTree entries={entries} /* ...required props */ visual={{ itemHeight: 28 }} />

// Variable height: taller rows for files that render inline metadata
const itemHeight = useCallback(
  (entry: DirectoryEntry) => (entry.type === 'file' ? 32 : 24),
  [],
);
<DirectoryTree entries={entries} /* ...required props */ visual={{ itemHeight }} />

TreeGrid (columns) mode

Pass the optional grid prop to turn the tree into a TreeGrid: the name/tree column is frozen on the left (indentation and connector lines stay confined there) while the remaining columns render as a vertically-aligned grid — with an aligned column header, an optional footer that aggregates over all entries (collapsed subtrees included), and a horizontally scrollable numeric region (the name column stays put).

columns[0] is always the name/tree column. grid.columns and every render / footer must be stable references (memoize them) — otherwise every visible row re-renders.

import { useMemo } from 'react';
import { DirectoryTree, type DirectoryTreeColumn, type DirectoryEntry } from '@aiquants/directory-tree';

const columns: DirectoryTreeColumn[] = useMemo(() => [
  // columns[0] = name/tree column. `render` output becomes the label; indent + glyph + icon
  // are drawn by the library. Omit `render` to fall back to entry.name.
  { key: 'name', header: 'Name', width: 260, render: (e) => e.name },
  { key: 'qty',  header: 'Qty',  width: 96, align: 'right', render: (e) => fmt(e.data.qty),
    footer: (all) => fmt(all.reduce((s, e) => s + e.data.qty, 0)) },
  { key: 'ratio', header: 'Ratio', width: 96, align: 'right', render: (e) => `${e.data.ratio}%` },
], []);

<DirectoryTree
  entries={entries}
  /* ...required expansion / selection props */
  grid={{ columns, showHeader: true, showFooter: true }}
/>

Grid mode exposes role="treegrid" / row / gridcell / columnheader semantics. Note that visual.removeRootIndent is ignored in grid mode (the frozen name column must start at x=0), and grid.scrollBarWidth defaults to the VirtualScroll scrollbar width so columns stay aligned. The grid styles are part of the component CSS artifacts — wire them up as shown in Styling (they are not a separate import).

Large Datasets

The component is optimized for large datasets through virtualization:

import { DirectoryTree } from '@aiquants/directory-tree';

// Handle thousands of entries efficiently
<DirectoryTree
  entries={largeDataset}
  // ... other required props
  visual={{
    style: { height: '600px' }
  }}
/>

Multiple Selection Mode

Enable multiple selection for batch operations:

const [selectedItems, setSelectedItems] = useState(new Set<string>());

const handleSelectionChange = (entry: DirectoryEntry, isSelected: boolean) => {
  setSelectedItems(prev => {
    const newSet = new Set(prev);
    if (isSelected) {
      newSet.add(entry.absolutePath);
    } else {
      newSet.delete(entry.absolutePath);
    }
    return newSet;
  });
};

<DirectoryTree
  // ... other props
  selection={{
    mode: "multiple",
    selectedItems: selectedItems,
    onSelectionChange: handleSelectionChange,
    onEntryClick: handleEntryClick // Required prop
  }}
/>

Custom Double-Click Behavior

Control how directories behave on double-click:

<DirectoryTree
  // ... other props
  expansion={{
    // ... required expansion props
    doubleClickAction: "toggle" // Only toggle the clicked directory
    // or
    doubleClickAction: "recursive" // Expand/collapse all children (default)
  }}
/>

State Persistence

The useDirectoryTreeState hook automatically persists expansion state to localStorage:

const { toggle, isExpanded, expandMultiple, collapseMultiple } = useDirectoryTreeState({
  storageKey: 'myapp-directory-tree',
  initialExpanded: new Set(['/src', '/docs'])
});

TypeScript Support

This package is written in TypeScript and provides comprehensive type definitions. All components and hooks are fully typed for the best development experience.

Contributing

We welcome contributions! Please feel free to submit issues and pull requests.

License

MIT License - see the LICENSE file for details.

Author


Made with ❤️ by the AIQuants team.