Tree

A collapsible, hierarchical list for organizing nested items like folders and files.

Folder 1
Folder 2
Folder 3
<TreeList
  nodes={folderNodes}
  defaultIcon={{ type: "lucide", name: "file-text" }}
  showEmptyChild
/>

Installation

pnpm add @notion-kit/tree

Examples


Group

Folder 1
Folder 2
Folder 3
<TreeGroup title="Workspace" description="Add a file">
  <TreeList
    nodes={folderNodes}
    defaultIcon={{ type: "lucide", name: "file-text" }}
    selectedId={activeFile}
    onSelect={setActiveFile}
    showEmptyChild
  />
</TreeGroup>

Custom Tree Item

Folder 1
Folder 2
Folder 3
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
"use client";
 
import React, { forwardRef } from "react";
import {
  ChevronDown,
  ChevronRight,
  MoreHorizontal,
  Plus,
  Trash,
} from "lucide-react";
 
import { cn } from "@notion-kit/cn";
import { IconBlock, type IconData } from "@notion-kit/icon-block";
import {
  Button,
  buttonVariants,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@notion-kit/shadcn";
 
export interface CustomItemProps {
  className?: string;
  label: string;
  icon?: IconData | null;
  lastEditedBy?: string;
  lastEditedAt?: string;
  id?: string;
  active?: boolean;
  expandable?: boolean;
  expanded?: boolean;
  level?: number;
  onClick?: () => void;
  onExpand?: () => void;
  onCreate?: () => void;
  onDelete?: (itemId: string) => void;
}
 
export const CustomItem = forwardRef<HTMLDivElement, CustomItemProps>(
  function Item(
    {
      className,
      id,
      label,
      icon = { type: "lucide", src: "file" },
      active,
      lastEditedBy = "admin",
      lastEditedAt = "now",
      level = 0,
      expandable = false,
      expanded,
      onClick,
      onExpand,
      onCreate,
      onDelete,
    },
    ref,
  ) {
    /** Events */
    const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
      e.stopPropagation();
      onExpand?.();
    };
    const handleCreate = (e: React.MouseEvent<HTMLDivElement>) => {
      e.stopPropagation();
      onCreate?.();
      if (!expanded) onExpand?.();
    };
    const handleDelete = (e: Event | React.SyntheticEvent) => {
      e.stopPropagation();
      if (id) onDelete?.(id);
    };
 
    return (
      <TooltipProvider>
        <div
          ref={ref}
          onClick={onClick}
          role="button"
          style={{ paddingLeft: `${(level + 1) * 12}px` }}
          className={cn(
            buttonVariants({ variant: null }),
            "group/item relative flex h-[27px] w-full justify-normal py-1 pr-3 font-medium text-secondary",
            active && "bg-default/10 text-primary",
            className,
          )}
        >
          <div className="group/icon">
            <Button
              variant="hint"
              className={cn(
                "relative hidden size-5 shrink-0 p-0.5",
                expandable && "group-hover/icon:flex",
              )}
              onClick={handleExpand}
            >
              {expanded ? <ChevronDown /> : <ChevronRight />}
            </Button>
            <IconBlock
              className={cn(expandable && "group-hover/icon:hidden")}
              icon={icon ?? { type: "text", src: label }}
            />
          </div>
          <span className="ml-1 truncate">{label}</span>
          {!!id && (
            <div className="ml-auto flex items-center p-0.5">
              <DropdownMenu>
                <Tooltip>
                  <TooltipTrigger asChild>
                    <DropdownMenuTrigger
                      onClick={(e) => e.stopPropagation()}
                      asChild
                    >
                      <div
                        role="button"
                        className={cn(
                          buttonVariants({
                            variant: "hint",
                            className:
                              "ml-auto size-auto p-0.5 opacity-0 group-hover/item:opacity-100",
                          }),
                        )}
                      >
                        <MoreHorizontal className="size-4" />
                      </div>
                    </DropdownMenuTrigger>
                  </TooltipTrigger>
                  <TooltipContent>
                    Delete, duplicate, and more...
                  </TooltipContent>
                </Tooltip>
                <DropdownMenuContent
                  className="w-60"
                  align="start"
                  side="right"
                  forceMount
                >
                  <DropdownMenuGroup>
                    <DropdownMenuItem
                      variant="warning"
                      Icon={<Trash className="size-4" />}
                      Body="Delete"
                      onSelect={handleDelete}
                    />
                  </DropdownMenuGroup>
                  <DropdownMenuSeparator />
                  <div className="flex flex-col items-center px-2 py-1 text-xs text-muted">
                    <div className="w-full">Last edited by: {lastEditedBy}</div>
                    <div className="w-full">{lastEditedAt}</div>
                  </div>
                </DropdownMenuContent>
              </DropdownMenu>
              {expandable && (
                <Tooltip>
                  <TooltipTrigger asChild>
                    <div
                      role="button"
                      onClick={handleCreate}
                      className={cn(
                        buttonVariants({
                          variant: "hint",
                          className:
                            "ml-auto size-auto rounded-sm p-0.5 opacity-0 group-hover/item:opacity-100",
                        }),
                      )}
                    >
                      <Plus className="size-4" />
                    </div>
                  </TooltipTrigger>
                  <TooltipContent>Add a page inside</TooltipContent>
                </Tooltip>
              )}
            </div>
          )}
        </div>
      </TooltipProvider>
    );
  },
);
 

API Reference

TreeList

A TreeList is a generic component TreeList<T>, where T extends TreeItemData.

PropTypeDefaultDescription
levelnumber0Current tree depth.
nodes*TreeNode<T>[]-The recursive tree list data.
defaultIconIconInfo-Default icon for tree item.
showEmptyChildbooleanfalseWhether the empty child should be displayed.
selectedIdstring | null-The focused tree item ID.
onSelect(id: string) => void-Handler that is called when a tree item is focused.
Item(props: TreeItemProps<T>) => React.ReactNodeTreeItemA custom renderer for each tree item.

TreeGroup

PropTypeDefaultDescription
title*string-The name of the group.
descriptionstring-The description of the group that will be shown as a tooltip.
isLoadingboolean-Whether the group is loading.
childrenReact.ReactNode-
onCreate() => void-Handler that is called when the "➕" button is clicked.

TreeItem

A TreeItem is a generic component TreeItem<T>, where T extends TreeItemData.

PropTypeDefaultDescription
node*T-The item data.
levelnumber0The current depth of the item in the tree.
expandableboolean-Whether the item is expandable.
expandedboolean-Whether the item is expanded.
isSelectedboolean-Whether the item is focused.
childrenReact.ReactNode-
onSelect() => void-Handler that is called when the item is focused.
onExpand() => void-Handler that is called when the expand/collapse icon is clicked.

type TreeItemData

PropTypeDefaultDescription
id*string-The ID of the tree item.
nodes*string-The name of the tree item.
parentIdstring | null-The ID of its parent.
iconIconData-The icon of the tree item.
groupstring | null-The group the item belongs to.

type TreeNode

A tree node is simply a tree item with its children.

type TreeNode<T extends TreeItemData> = T & { children: TreeNode<T>[] };

On this page