Skip to content

Tree

DisplayMIT
View in Storybook

Recursive expandable tree view with selection, drag-reorder, and virtualization for 1000+ nodes.

bun add @otfdashkit/ui

Preview

Tree
Loading Storybook preview…

Code

import { useMemo, useState } from 'react'
import { File, FolderClosed, FolderOpen } from 'lucide-react'
import { Tree, type TreeNode } from '@otfdashkit/ui'

function withFolderIcons(nodes: TreeNode[], expanded: Set<string>): TreeNode[] {
  return nodes.map((n) => {
    const isFolder = !!n.children
    const icon = isFolder
      ? expanded.has(n.id)
        ? <FolderOpen className="h-3.5 w-3.5" strokeWidth={1.75} />
        : <FolderClosed className="h-3.5 w-3.5" strokeWidth={1.75} />
      : <File className="h-3.5 w-3.5" strokeWidth={1.75} />
    return {
      ...n,
      icon,
      children: n.children ? withFolderIcons(n.children, expanded) : undefined,
    }
  })
}

const fileExplorerData: TreeNode[] = [
  {
    id: 'src',
    label: 'src',
    children: [
      {
        id: 'src/components',
        label: 'components',
        children: [
          { id: 'src/components/Button.tsx', label: 'Button.tsx' },
          { id: 'src/components/Card.tsx', label: 'Card.tsx' },
          { id: 'src/components/Modal.tsx', label: 'Modal.tsx' },
        ],
      },
      {
        id: 'src/hooks',
        label: 'hooks',
        children: [
          { id: 'src/hooks/useDebounce.ts', label: 'useDebounce.ts' },
          { id: 'src/hooks/useMedia.ts', label: 'useMedia.ts' },
        ],
      },
      { id: 'src/index.ts', label: 'index.ts' },
      { id: 'src/app.tsx', label: 'app.tsx' },
    ],
  },
  {
    id: 'public',
    label: 'public',
    children: [
      { id: 'public/favicon.svg', label: 'favicon.svg' },
      { id: 'public/logo.png', label: 'logo.png' },
    ],
  },
  { id: 'package.json', label: 'package.json' },
  { id: 'README.md', label: 'README.md' },
  { id: 'tsconfig.json', label: 'tsconfig.json' },
]

export function Example() {
  const [expanded, setExpanded] = useState<string[]>(['src', 'src/components'])
  const data = useMemo(
    () => withFolderIcons(fileExplorerData, new Set(expanded)),
    [expanded]
  )
  return (
    <div className="max-w-sm rounded-lg border border-border bg-card p-2">
      <Tree
        data={data}
        expandedIds={expanded}
        onExpandedChange={setExpanded}
      />
    </div>
  )
}

Free with the SDK. This component ships in @otfdashkit/ui under the MIT license.

Props

Props declared in the Tree source. Inherited HTML attributes are not listed.

PropTypeDefaultDescription
datarequiredTreeNode[]
defaultExpandedIdsstring[]
estimatedRowHeightnumber32Used when virtualize=true. Default 32.
expandedIdsstring[]Controlled expanded ids.
onExpandedChange(ids: string[]) => void
onMove(sourceId: string, targetId: string, position: TreeMovePosition) => voidDefine to enable drag-reorder. Omit to disable drag entirely.
onSelect(node: TreeNode) => void
selectedIdstring
showLinesbooleantrueShow vertical guide lines from caret to children. Default true.
virtualizebooleanfalseUse @tanstack/react-virtual to virtualize visible rows. Default false.