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>[] };