Shadcn Treeview
Examples

Search & Filter

Tree view with search functionality

This example shows how to implement search/filter functionality.

Code

import * as React from "react";
import { TreeView, TreeViewItem, flattenTree } from "shadcn-treeview";
import { FileIcon, FolderIcon, FolderOpenIcon, SearchIcon } from "lucide-react";

type TreeNode = {
  id: string;
  name: string;
  children?: TreeNode[];
  parent?: string;
  isBranch?: boolean;
};

export function SearchTreeView() {
  const treeData: TreeNode = React.useMemo(
    () => ({
      id: "root",
      name: "Root",
      children: [
        {
          id: "1",
          parent: "root",
          name: "Fruits",
          isBranch: true,
          children: [
            { id: "11", name: "Apple", parent: "1" },
            { id: "12", name: "Banana", parent: "1" },
            { id: "13", name: "Cherry", parent: "1" }
          ]
        },
        {
          id: "2",
          parent: "root",
          name: "Vegetables",
          isBranch: true,
          children: [
            { id: "21", name: "Carrot", parent: "2" },
            { id: "22", name: "Broccoli", parent: "2" },
            { id: "23", name: "Spinach", parent: "2" }
          ]
        }
      ]
    }),
    []
  );

  const flatData = React.useMemo(() => flattenTree(treeData), [treeData]);
  const [searchTerm, setSearchTerm] = React.useState("");
  const [filteredData, setFilteredData] = React.useState(treeData);

  const filterTree = React.useCallback(
    (searchValue: string): TreeNode => {
      const upperValue = searchValue.toUpperCase();
      const filteredNodes: Set<string> = new Set();

      // Include parents of matching nodes
      const includeParents = (nodeId: string) => {
        const node = flatData.find((item) => item.id === nodeId);
        if (node && !filteredNodes.has(node.id.toString())) {
          filteredNodes.add(node.id.toString());
          if (node.parent) {
            includeParents(node.parent.toString());
          }
        }
      };

      // Find matching nodes
      flatData.forEach((item) => {
        if (item.name.toUpperCase().includes(upperValue)) {
          includeParents(item.id.toString());
        }
      });

      // Build filtered tree
      const buildFilteredTree = (nodeId: string): TreeNode | undefined => {
        const node = flatData.find((item) => item.id === nodeId);
        if (!node || !filteredNodes.has(node.id.toString())) return undefined;
        return {
          id: node.id.toString(),
          parent: node.parent?.toString(),
          name: node.name,
          isBranch: node.isBranch,
          children: node.children
            ?.map((child) => buildFilteredTree(child.toString()))
            .filter(Boolean) as TreeNode[]
        };
      };

      return buildFilteredTree("root")!;
    },
    [flatData]
  );

  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newSearchTerm = e.target.value;
    setSearchTerm(newSearchTerm);
    setFilteredData(newSearchTerm ? filterTree(newSearchTerm) : treeData);
  };

  return (
    <div className="w-64 space-y-2">
      <div className="relative">
        <SearchIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
        <input
          type="text"
          placeholder="Search..."
          value={searchTerm}
          onChange={handleSearchChange}
          className="w-full rounded border bg-background py-2 pl-8 pr-3 text-sm"
        />
      </div>

      <TreeView
        data={flattenTree(filteredData)}
        aria-label="Searchable tree"
        className="rounded border p-2"
        nodeRenderer={({
          element,
          isBranch,
          isExpanded,
          isSelected,
          getNodeProps,
          level
        }) => (
          <TreeViewItem
            {...getNodeProps()}
            name={element.name}
            isBranch={isBranch}
            isExpanded={isExpanded}
            isSelected={isSelected}
            level={level}
            indentation={16}
            icon={
              isBranch ? (
                isExpanded ? (
                  <FolderOpenIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
                ) : (
                  <FolderIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
                )
              ) : (
                <FileIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
              )
            }
          />
        )}
      />
    </div>
  );
}

On this page