Shadcn Pro

Tree View

Tree View

A tree view that assembles all the functionalities of the Accordion component to create a tree view.

Installation

Install the following dependencies:

npm install @radix-ui/react-accordion
npm install use-resize-observer
npm install @tanstack/react-virtual
npx shadcn-ui@latest add scroll-area

Copy and paste the following code into your project.

"use client";
 
import { cn } from "@/lib/utils";
import React, { forwardRef, useCallback, useRef } from "react";
import useResizeObserver from "use-resize-observer";
import { useVirtualizer } from "@tanstack/react-virtual";
import {
  Tree,
  Folder,
  File,
  CollapseButton,
  TreeViewElement,
} from "./tree-view-api";
 
// TODO: Add the ability to add custom icons
 
interface TreeViewComponentProps extends React.HTMLAttributes<HTMLDivElement> {}
 
type TreeViewProps = {
  initialSelectedId?: string;
  elements: TreeViewElement[];
  indicator?: boolean;
} & (
  | {
      initialExpendedItems?: string[];
      expandAll?: false;
    }
  | {
      initialExpendedItems?: undefined;
      expandAll: true;
    }
) &
  TreeViewComponentProps;
 
export const TreeView = ({
  elements,
  className,
  initialSelectedId,
  initialExpendedItems,
  expandAll,
  indicator = false,
}: TreeViewProps) => {
  const containerRef = useRef<HTMLDivElement>(null);
 
  const { getVirtualItems, getTotalSize } = useVirtualizer({
    count: elements.length,
    getScrollElement: () => containerRef.current,
    estimateSize: useCallback(() => 40, []),
    overscan: 5,
  });
 
  const { height = getTotalSize(), width } = useResizeObserver({
    ref: containerRef,
  });
  return (
    <div
      ref={containerRef}
      className={cn(
        "w-full rounded-md overflow-hidden py-1 relative",
        className
      )}
    >
      <Tree
        initialSelectedId={initialSelectedId}
        initialExpendedItems={initialExpendedItems}
        elements={elements}
        style={{ height, width }}
        className="w-full h-full overflow-y-auto"
      >
        {getVirtualItems().map((element:any) => (
          <TreeItem
            aria-label="Root"
            key={element.key}
            elements={[elements[element.index]]}
            indicator={indicator}
          />
        ))}
        <CollapseButton elements={elements} expandAll={expandAll}>
          <span>Expand All</span>
        </CollapseButton>
      </Tree>
    </div>
  );
};
 
TreeView.displayName = "TreeView";
 
export const TreeItem = forwardRef<
  HTMLUListElement,
  {
    elements?: TreeViewElement[];
    indicator?: boolean;
  } & React.HTMLAttributes<HTMLUListElement>
>(({ className, elements, indicator, ...props }, ref) => {
  return (
    <ul ref={ref} className="w-full space-y-1 " {...props}>
      {elements &&
        elements.map((element) => (
          <li key={element.id} className="w-full">
            {element.children && element.children?.length > 0 ? (
              <Folder
                element={element.name}
                value={element.id}
                isSelectable={element.isSelectable}
              >
                <TreeItem
                  key={element.id}
                  aria-label={`folder ${element.name}`}
                  elements={element.children}
                  indicator={indicator}
                />
              </Folder>
            ) : (
              <File
                value={element.id}
                aria-label={`File ${element.name}`}
                key={element.id}
                isSelectable={element.isSelectable}
              >
                <span>{element?.name}</span>
              </File>
            )}
          </li>
        ))}
    </ul>
  );
});
 
TreeItem.displayName = "TreeItem";
 

Update tailwind.config.ts

tailwind.config.ts
/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    extend: {
      keyframes: { 
        "accordion-down": { 
          from: { height: "0" },
          to: { height: "var(--radix-accordion-content-height)" },
        },
        "accordion-up": {
          from: { height: "var(--radix-accordion-content-height)" },
          to: { height: "0" },
        },
      },
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
    },
  },
};
 

Why there is two components?

The TreeView component has two different ways to be used, that you can choose , based on your choice , that you have picked in the installation section.

  • The first approach is the tree-view component that is based on the tree-view-api components, in other words it is a wrapper for devs that don't want to have an custom functionality, but you still have the access to customise the ui , some of the benifits are :

    • Full control over the UI.
    • All functionality is provided out of the box , including the (Virtualization , Selection , Expend/Collapse , Keyboard Navigation).
    • You can customise the functionality if you want.
  • The second approach is the tree-view-api component that is based on the radix-ui-accordion components, and it is the primitive api that you can use to build your own custom tree view, some of the benifits are:

    • Full control over the UI.
    • You can customise the functionality as you want.

API Reference

proptypedefault value
initialSelectedId
string
--
expendAll
boolean
false
elements*
TreeViewElement[]
--
initialExpendedItems
string[]
--
openIcon
ReactNode
<FolderOpenIcon />
closeIcon
ReactNode
<FolderIcon />
fileIcon
ReactNode
<FileIcon />
Heads up
The initialExpendedItems with expendAll can not work together, if you want to expend all the items you should use expendAll prop only , otherwase if you want only specific items , provide initialExpendedItems prop.

TreeViewElement

The TreeViewElement object accepts the following props:

fieldtypedefault Value
id*
string
--
name*
string
--
children
TreeViewElement[]
--
isSelectable
boolean
--

Accessibility

Currently, the TreeView component is fully accessible and supports keyboard navigation.

Keyboard Navigation

keyDescription
Tab
enter a nested folder
Tab + Shift
exit the current selected folder
ArrowDown
Increments by one step horzintanly
ArrowUp
Decrements by one step horzintanly
Enter
Opens/Close a folder , select a file

Usage

The TreeView component has two different ways to be used, that you can choose , based on your choice , that you have picked in the installation section.

import { TreeView } from "@/components/extension/tree-view";
const elements = [
  {
    id: "1",
    name: "components",
    children: [
      {
        id: "2",
        name: "extension",
        children: [
          {
            id: "3",
            name: "tree-view.tsx",
          },
          {
            id: "4",
            name: "tree-view-api.tsx",
          },
        ],
      },
      {
        id: "5",
        name: "dashboard-tree.tsx",
      },
    ],
  },
];
export default function TreeViewExample() {
  return (
    <TreeView
      elements={elements}
      initialSelectedId="3"
      initialExpendedItems={["1", "2"]}
    />
  );
}
 

Further Reading

On this page