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

clack-tree-select

v1.0.8

Published

Beautiful, interactive, searchable tree selection prompt for Clack CLI apps

Downloads

729

Readme

🌳 clack-tree-select

npm version npm downloads license


Beautiful, interactive tree selection prompt plugin for Clack-based command-line applications. Built for use specifically with Clack.

🎬 Demo

Tree Select Demo

▶️ Watch Demo on Asciinema

See navigation, hierarchical selection, and smart keyboard shortcuts in action.

✨ Features

  • 🌳 Hierarchical Selection - Select parent directories to select all children
  • ⌨️ Smart Keyboard Shortcuts - Intuitive navigation with toggle expand/select all
  • 🔎 Type-to-Search (new) - Start typing to filter items in real time; Esc clears
  • 🎨 Customizable Styling - Beautiful icons and colors that match your brand
  • 🚀 Performance Optimized - Efficient rendering for large directory trees
  • 📱 TypeScript Ready - Full type safety with excellent IntelliSense
  • 🔧 File System Integration - Built-in support for browsing local directories
  • Validation Support - Custom validation with helpful error messages

📦 Installation

npm install clack-tree-select
yarn add clack-tree-select
pnpm add clack-tree-select

🚀 Quick Start

Add tree selection to your existing Clack CLI:

import { treeSelect } from 'clack-tree-select';
import { intro, outro, text, isCancel } from '@clack/prompts';

async function main() {
  intro('🚀 Project Setup');

  // Your existing Clack prompts
  const projectName = await text({
    message: 'Project name?',
    placeholder: 'my-project'
  });

  if (isCancel(projectName)) return;

  // Add tree selection seamlessly
  const files = await treeSelect({
    message: 'Select files to process:',
    tree: [
      {
        value: 'src',
        name: 'src',
        children: [
          { value: 'src/components', name: 'components' },
          { value: 'src/pages', name: 'pages' },
          { value: 'src/utils', name: 'utils' }
        ]
      },
      { value: 'package.json', name: 'package.json' }
    ]
  });

  if (isCancel(files)) return;

  outro(`✅ ${projectName}: Selected ${files.length} files`);
}

main();

Perfect integration - works alongside all your existing Clack prompts! 🎯

🌲 Beyond Files: Any Tree Data Structure

While clack-tree-select works great with file systems, it's designed as a generic tree selection tool. You can create hierarchical selection prompts for any data structure! Here are some powerful examples:

🎨 UI Component Library Selection

const components = await treeSelect({
  message: 'Select UI components to include:',
  tree: [
    {
      value: 'forms',
      name: 'Form Components',
      children: [
        { value: 'forms/input', name: 'Text Input' },
        { value: 'forms/select', name: 'Select Dropdown' },
        { value: 'forms/checkbox', name: 'Checkbox' },
        { value: 'forms/radio', name: 'Radio Button' }
      ]
    },
    {
      value: 'navigation',
      name: 'Navigation',
      children: [
        { value: 'nav/header', name: 'Header' },
        { value: 'nav/sidebar', name: 'Sidebar' },
        { value: 'nav/breadcrumb', name: 'Breadcrumb' },
        { value: 'nav/pagination', name: 'Pagination' }
      ]
    },
    {
      value: 'feedback',
      name: 'Feedback',
      children: [
        { value: 'feedback/alert', name: 'Alert' },
        { value: 'feedback/toast', name: 'Toast' },
        { value: 'feedback/modal', name: 'Modal' },
        { value: 'feedback/tooltip', name: 'Tooltip' }
      ]
    }
  ]
});

🏢 Organization/Department Structure

interface Department {
  id: string;
  name: string;
  manager?: string;
}

const departments = await treeSelect<Department>({
  message: 'Select departments for the training program:',
  tree: [
    {
      value: { id: 'eng', name: 'Engineering', manager: 'Alice' },
      name: 'Engineering',
      children: [
        { value: { id: 'frontend', name: 'Frontend' }, name: 'Frontend Team' },
        { value: { id: 'backend', name: 'Backend' }, name: 'Backend Team' },
        { value: { id: 'devops', name: 'DevOps' }, name: 'DevOps Team' }
      ]
    },
    {
      value: { id: 'product', name: 'Product', manager: 'Bob' },
      name: 'Product',
      children: [
        { value: { id: 'design', name: 'Design' }, name: 'Design Team' },
        { value: { id: 'pm', name: 'Product Management' }, name: 'Product Managers' },
        { value: { id: 'research', name: 'User Research' }, name: 'Research Team' }
      ]
    }
  ]
});

🛍️ E-commerce Category Selection

const categories = await treeSelect({
  message: 'Select product categories to feature:',
  tree: [
    {
      value: 'electronics',
      name: 'Electronics',
      children: [
        {
          value: 'computers',
          name: 'Computers',
          children: [
            { value: 'laptops', name: 'Laptops' },
            { value: 'desktops', name: 'Desktops' },
            { value: 'tablets', name: 'Tablets' }
          ]
        },
        {
          value: 'phones',
          name: 'Mobile Phones',
          children: [
            { value: 'smartphones', name: 'Smartphones' },
            { value: 'accessories', name: 'Phone Accessories' }
          ]
        }
      ]
    },
    {
      value: 'clothing',
      name: 'Clothing',
      children: [
        { value: 'mens', name: "Men's Clothing" },
        { value: 'womens', name: "Women's Clothing" },
        { value: 'kids', name: "Kids' Clothing" }
      ]
    }
  ]
});

📋 Feature/Permission Selection

interface Permission {
  id: string;
  scope: string;
  level: 'read' | 'write' | 'admin';
}

const permissions = await treeSelect<Permission>({
  message: 'Select permissions for this role:',
  tree: [
    {
      value: { id: 'users', scope: 'users', level: 'admin' },
      name: 'User Management',
      children: [
        { 
          value: { id: 'users-read', scope: 'users', level: 'read' }, 
          name: 'View Users' 
        },
        { 
          value: { id: 'users-write', scope: 'users', level: 'write' }, 
          name: 'Edit Users' 
        },
        { 
          value: { id: 'users-admin', scope: 'users', level: 'admin' }, 
          name: 'Manage Users' 
        }
      ]
    },
    {
      value: { id: 'content', scope: 'content', level: 'admin' },
      name: 'Content Management',
      children: [
        { 
          value: { id: 'content-read', scope: 'content', level: 'read' }, 
          name: 'View Content' 
        },
        { 
          value: { id: 'content-write', scope: 'content', level: 'write' }, 
          name: 'Edit Content' 
        }
      ]
    }
  ]
});

🎯 Key Benefits for Non-File Data

  • Type Safety: Full TypeScript support with custom data types
  • Flexible Values: Use strings, objects, numbers, or any data type as values
  • Custom Display: name property controls what users see, value is what you get back
  • Rich Hierarchies: Create deep, meaningful organizational structures
  • Batch Selection: Select entire categories and all children automatically

The generic TreeItem<T> interface makes it perfect for any hierarchical data structure in your applications!

📚 API Reference

treeSelect(options)

Creates an interactive tree selection prompt.

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | message | string | required | The prompt message to display | | tree | TreeItem[] | required | The tree structure to display | | multiple | boolean | true | Allow multiple selections | | initialValues | T[] | [] | Pre-selected values | | required | boolean | false | Require at least one selection | | maxItems | number | undefined | Maximum visible items (scrolling) | | searchable | boolean | true | Enable inline search. Type to filter; Esc clears | | searchDirectoriesOnly | boolean | false | When searching, only match folders (items with children), not files | | icons | IconOptions | default icons | Custom icons for tree items | | showHelp | boolean | true | Show keyboard shortcuts in validation |

TreeItem Interface

interface TreeItem<T = any> {
  value: T;              // Unique identifier
  name?: string;         // Display name (falls back to value)
  open?: boolean;        // Initially expanded
  children?: TreeItem<T>[] | string[] | T[];  // Child items
}

Icon Options

interface IconOptions {
  directory?: string;    // Default: '📁'
  file?: string;         // Default: '📄'  
  expanded?: string;     // Default: '▼'
  collapsed?: string;    // Default: '▶'
}

🔎 Using the searchable option

Type-to-search is enabled by default. You can explicitly enable/disable it per prompt:

import { treeSelect } from 'clack-tree-select';

// Explicitly enable (default)
await treeSelect({
  message: 'Pick files (type to search, Esc to clear):',
  tree: myTree,
  searchable: true,
});

// Disable type-to-search
await treeSelect({
  message: 'Pick files (search disabled):',
  tree: myTree,
  searchable: false,
});

Behavior when enabled:

  • Start typing to filter items across the entire hierarchy
  • Backspace edits the query; Esc clears/exits search
  • Arrow keys and Space operate over the filtered list

🔎 Using the searchDirectoriesOnly option

When you want search to only match folders (items with children) and not files, use searchDirectoriesOnly:

import { treeSelect } from 'clack-tree-select';

// Search matches ALL items (default behavior)
await treeSelect({
  message: 'Select items:',
  tree: myTree,
  searchable: true,
  searchDirectoriesOnly: false, // default
});

// Search matches ONLY folders/directories
await treeSelect({
  message: 'Select folders to ignore:',
  tree: myTree,
  searchable: true,
  searchDirectoriesOnly: true,
});

This is useful when you have a flat list of folders and want users to quickly find specific folders without matching file names. When searchDirectoriesOnly is true:

  • Only items with children property are matched during search
  • Files/leaf nodes are filtered out of search results
  • When search is cleared (Esc), all items are shown again

fileSystemTreeSelect(options)

Creates a tree selection prompt from a file system directory.

const files = await fileSystemTreeSelect({
  message: 'Select files from your project:',
  root: './src',
  includeFiles: true,
  includeHidden: false,
  maxDepth: 3,
  filter: (path) => !path.includes('node_modules')
});

⌨️ Keyboard Shortcuts

| Shortcut | Action | |----------|--------| | / | Navigate up/down | | / | Collapse/expand directory | | Space | Toggle selection | | type | Start search and filter visible items | | Backspace | Delete last character in search | | Esc | Clear search / exit search | | Shift+E | Toggle expand/collapse all | | Shift+A | Toggle select/deselect all | | Enter | Submit selection | | Ctrl+C | Cancel prompt |

🎯 Advanced Examples

Custom Icons and Styling

const result = await treeSelect({
  message: 'Choose components to generate:',
  tree: components,
  icons: {
    directory: '📂',
    file: '⚛️',
    expanded: '📂', 
    collapsed: '📁'
  },
  multiple: true,
  required: true
});

Single Selection Mode

const configFile = await treeSelect({
  message: 'Choose a configuration file:',
  tree: [
    {
      value: 'config',
      name: 'config',
      open: true,
      children: [
        { value: 'tsconfig.json', name: 'TypeScript Config' },
        { value: 'package.json', name: 'Package Config' },
        { value: 'vite.config.js', name: 'Vite Config' }
      ]
    }
  ],
  multiple: false,
  required: true
});

Note: In single selection mode, only leaf nodes (files without children) can be selected. Parent directories are shown but not selectable, ensuring users select actual items rather than containers.

File System Integration

import { fileSystemTreeSelect } from 'clack-tree-select';

const selectedFiles = await fileSystemTreeSelect({
  message: 'Select files to process:',
  root: process.cwd(),
  includeFiles: true,
  includeHidden: false,
  maxDepth: 3,
  filter: (path) => {
    // Exclude common non-source directories
    return !path.includes('node_modules') && 
           !path.includes('.git') && 
           !path.includes('dist');
  }
});

Custom Validation

const result = await treeSelect({
  message: 'Select at least 2 components:',
  tree: componentTree,
  validate: (selected) => {
    if (!selected || selected.length < 2) {
      return 'Please select at least 2 components to continue.';
    }
  }
});

🔧 Integration Examples

With Package Managers

// Ask user to select packages to install
const packages = await treeSelect({
  message: 'Select packages to install:',
  tree: [
    {
      value: 'react',
      name: 'React',
      children: [
        { value: 'react-dom', name: 'React DOM' },
        { value: 'react-router', name: 'React Router' }
      ]
    },
    {
      value: 'build-tools',
      name: 'Build Tools',
      children: [
        { value: 'vite', name: 'Vite' },
        { value: 'typescript', name: 'TypeScript' }
      ]
    }
  ]
});

With Monorepos

// Select packages in a monorepo
const workspaces = await fileSystemTreeSelect({
  message: 'Select workspaces to build:',
  root: './packages',
  includeFiles: false,  // Only directories
  maxDepth: 1,
  filter: (path) => {
    // Only include directories with package.json
    return fs.existsSync(`${path}/package.json`);
  }
});

🎨 Theming

The tree select automatically adapts to your terminal's color scheme and follows Clack's beautiful styling conventions:

  • Selected items: Green checkboxes (◼)
  • Unselected items: Gray checkboxes (◻)
  • Active item: Cyan highlighting
  • Directories: Folder icons with expand/collapse indicators
  • Files: Document icons

🔗 Integration with Existing Clack CLIs

This package is designed as a drop-in addition to your existing Clack CLI. No changes needed to your current setup!

// Your existing CLI
import { intro, text, select, multiselect, outro } from '@clack/prompts';
// Just add this import ↓
import { treeSelect } from 'clack-tree-select';

async function myCLI() {
  intro('My Existing CLI');
  
  const name = await text({ message: 'Project name?' });
  const framework = await select({ /* ... */ });
  
  // New: Add tree selection anywhere in your flow
  const files = await treeSelect({
    message: 'Select files to process:',
    tree: myFileTree
  });
  
  const tools = await multiselect({ /* ... */ });
  outro('Done!');
}

Zero breaking changes - just add treeSelect where you need hierarchical selection!

🧪 Examples

Check out the examples/ directory for interactive demos:

cd examples
pnpm install

# Basic tree-select functionality
pnpm tree-select

# Complete integration with other Clack prompts
pnpm integration

# Searchable demo (type to filter, Esc to clear)
pnpm run search

The integration demo shows a real CLI workflow mixing treeSelect with text, select, multiselect, and confirm prompts.

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📝 License

This project is licensed under the MIT License - see the LICENSE file for details.