Tree View

Component

Beautifully animated Tailwind tree view components for rendering complex hierarchical data and Tailwind CSS folder structure UI.

Install via CLI

Run this command to automatically add the component and its dependencies to your project.

npx @abhaysinghr516/business-wish add tree-view
New to the CLI? Run npx @abhaysinghr516/business-wish init first to initialize your project.

Organize deep hierarchical data beautifully with a Tailwind tree view. Whether you are building a full-featured file explorer or navigating complex categories, these Tailwind CSS folder structure components support nested collapsibility, clean indentation, and aesthetics.

A tree view is the perfect companion for a robust Sidebar navigation scheme, serving as a more scalable alternative to a standard Accordion for deeply nested relationships.

Basic Minimal Tree View

import React, { useState } from "react";
import { ChevronRight, Folder, File, Component, FileImage, LayoutGrid, Terminal, Database, Shield, Image as ImageIcon } from "lucide-react";

const defaultTreeData = [
  {
    id: "src",
    name: "src",
    type: "folder",
    icon: <Folder className="w-4 h-4 text-blue-500" />,
    children: [
      {
        id: "components",
        name: "components",
        type: "folder",
        icon: <Component className="w-4 h-4 text-indigo-500" />,
        children: [
          { id: "Button.tsx", name: "Button.tsx", type: "file", icon: <File className="w-4 h-4 text-neutral-400" /> },
          { id: "Card.tsx", name: "Card.tsx", type: "file", icon: <File className="w-4 h-4 text-neutral-400" /> },
          { id: "Input.tsx", name: "Input.tsx", type: "file", icon: <File className="w-4 h-4 text-neutral-400" /> }
        ]
      },
      // ... more nodes
    ]
  },
  { id: "package.json", name: "package.json", type: "file", icon: <File className="w-4 h-4 text-neutral-400" /> },
  { id: "README.md", name: "README.md", type: "file", icon: <File className="w-4 h-4 text-neutral-400" /> }
];

const BasicTreeNode = ({ node, level }) => {
  const [isOpen, setIsOpen] = useState(false);
  const isFolder = node.type === "folder";

  return (
    <div className="w-full">
      <div 
        className={`flex items-center py-1.5 px-2 rounded-lg cursor-pointer transition-colors ${
          level === 0 ? "hover:bg-neutral-100 dark:hover:bg-neutral-900" : "hover:bg-neutral-50 dark:hover:bg-neutral-900/50"
        }`}
        style={{ paddingLeft: `${level * 16 + 8}px` }}
        onClick={() => isFolder && setIsOpen(!isOpen)}
      >
        <span className="w-4 h-4 mr-1 flex items-center justify-center">
          {isFolder && (
            <ChevronRight 
              className={`w-3.5 h-3.5 text-neutral-400 dark:text-neutral-500 transition-transform duration-200 ${isOpen ? "rotate-90" : ""}`} 
            />
          )}
        </span>
        {node.icon ? (
          <span className="mr-2">{node.icon}</span>
        ) : (
          isFolder ? <Folder className="w-4 h-4 mr-2 text-blue-500" /> : <File className="w-4 h-4 mr-2 text-neutral-500" />
        )}
        <span className="text-[13px] font-medium text-neutral-700 dark:text-neutral-300 truncate">
          {node.name}
        </span>
      </div>
      
      {isFolder && (
        <div 
          className="overflow-hidden transition-all duration-300 ease-in-out"
          style={{ maxHeight: isOpen ? "1000px" : "0", opacity: isOpen ? 1 : 0 }}
        >
          {node.children?.map(child => (
            <BasicTreeNode key={child.id} node={child} level={level + 1} />
          ))}
        </div>
      )}
    </div>
  );
};

const BasicTreeView = ({ data = defaultTreeData }) => {
  return (
    <div className="w-full max-w-sm bg-white dark:bg-[#0A0A0A] border border-neutral-200 dark:border-white/10 rounded-2xl p-4 shadow-sm">
      <h3 className="text-[14px] font-bold text-neutral-900 dark:text-white px-2 mb-3 tracking-tight">Project Explorer</h3>
      <div className="flex flex-col space-y-0.5">
        {data.map(node => (
          <BasicTreeNode key={node.id} node={node} level={0} />
        ))}
      </div>
    </div>
  );
};

export default BasicTreeView;

MacOS Finder Style Tree View

import React, { useState } from "react";
import { ChevronRight, File } from "lucide-react";
// tree data omitted for brevity

const FinderTreeNode = ({ node, level, activeId, onSelect }) => {
  const [isOpen, setIsOpen] = useState(level === 0);
  const isFolder = node.type === "folder";
  const isActive = activeId === node.id;

  return (
    <div className="w-full font-sans">
      <div 
        className={`flex items-center py-1 rounded-md cursor-pointer transition-colors ${
          isActive 
            ? "bg-blue-500 text-white" 
            : "hover:bg-neutral-100 dark:hover:bg-neutral-800 text-neutral-800 dark:text-neutral-200"
        }`}
        style={{ paddingLeft: `${level * 20 + 4}px` }}
        onClick={() => {
          onSelect(node.id);
          if (isFolder) setIsOpen(!isOpen);
        }}
        onDoubleClick={() => isFolder && setIsOpen(!isOpen)}
      >
        <span className="w-4 h-4 mr-1 flex items-center justify-center">
          {isFolder && (
            <ChevronRight 
              className={`w-3 h-3 transition-transform duration-100 ${isOpen ? "rotate-90" : ""} ${isActive ? "text-white" : "text-neutral-400"}`} 
              strokeWidth={3}
            />
          )}
        </span>
        {isFolder ? (
           <svg className={`w-4 h-4 mr-2 flex-shrink-0 ${isActive ? "text-white" : "text-blue-400"}`} viewBox="0 0 24 24" fill="currentColor">
            <path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
          </svg>
        ) : (
          <File className={`w-4 h-4 mr-2 flex-shrink-0 ${isActive ? "text-white/80" : "text-neutral-400"}`} strokeWidth={2} />
        )}
        <span className="text-[13px] truncate select-none leading-relaxed tracking-tight">
          {node.name}
        </span>
      </div>
      
      {isFolder && isOpen && (
        <div className="flex flex-col">
          {node.children?.map(child => (
            <FinderTreeNode key={child.id} node={child} level={level + 1} activeId={activeId} onSelect={onSelect} />
          ))}
        </div>
      )}
    </div>
  );
};

const FinderTreeView = ({ data = defaultTreeData }) => {
  const [activeId, setActiveId] = useState("src");

  return (
    <div className="w-full max-w-sm bg-[#fafafa] dark:bg-[#1e1e1e] border border-neutral-300 dark:border-neutral-800 rounded-xl overflow-hidden shadow-lg shadow-neutral-200/50 dark:shadow-none flex flex-col">
      <div className="h-10 bg-gradient-to-b from-white to-[#f0f0f0] dark:from-[#323232] dark:to-[#282828] border-b border-neutral-300 dark:border-neutral-800 flex items-center px-4">
        <div className="flex gap-2 mr-4">
          <div className="w-3 h-3 rounded-full bg-[#ff5f56] border border-[#e0443e]" />
          <div className="w-3 h-3 rounded-full bg-[#ffbd2e] border border-[#dea123]" />
          <div className="w-3 h-3 rounded-full bg-[#27c93f] border border-[#1aab29]" />
        </div>
        <span className="text-[12px] font-semibold text-neutral-600 dark:text-neutral-400 mx-auto select-none">my-app</span>
      </div>
      
      <div className="p-2 overflow-y-auto max-h-[400px]">
        {data.map(node => (
          <FinderTreeNode key={node.id} node={node} level={0} activeId={activeId} onSelect={setActiveId} />
        ))}
      </div>
    </div>
  );
};

export default FinderTreeView;

Connected Line Tree View

import React, { useState } from "react";
// tree data omitted for brevity

const LineTreeNode = ({ node, level, isLast }) => {
  const [isOpen, setIsOpen] = useState(true);
  const isFolder = node.type === "folder";

  return (
    <div className="w-full relative">
      {level > 0 && (
        <div 
          className="absolute border-l border-neutral-200 dark:border-neutral-800" 
          style={{ left: `${level * 24 - 12}px`, top: "-12px", bottom: isLast ? "16px" : "-12px" }} 
        />
      )}
      {level > 0 && (
        <div 
          className="absolute border-t border-neutral-200 dark:border-neutral-800 w-3" 
          style={{ left: `${level * 24 - 12}px`, top: "16px" }} 
        />
      )}

      <div 
        className="flex items-center py-1.5 rounded-md cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-900/50 transition-colors w-fit pr-4"
        style={{ paddingLeft: `${level * 24}px` }}
        onClick={() => isFolder && setIsOpen(!isOpen)}
      >
        {isFolder ? (
          <div className="w-5 h-5 flex items-center justify-center mr-2 bg-neutral-100 dark:bg-neutral-800 rounded border border-neutral-200 dark:border-neutral-700 z-10 transition-colors hover:border-neutral-300 dark:hover:border-neutral-600">
             <span className="text-[14px] font-mono text-neutral-500 leading-none pb-0.5">{isOpen ? "-" : "+"}</span>
          </div>
        ) : (
          <div className="w-5 h-5 mr-2 flex-shrink-0 z-10" />
        )}
        <span className="text-[14px] text-neutral-700 dark:text-neutral-300 truncate">
          {node.name}
        </span>
      </div>
      
      {isFolder && isOpen && (
        <div className="flex flex-col pb-1">
          {node.children?.map((child, index) => (
             <LineTreeNode key={child.id} node={child} level={level + 1} isLast={index === node.children.length - 1}  />
          ))}
        </div>
      )}
    </div>
  );
};

const LineTreeView = ({ data = defaultTreeData }) => {
  return (
    <div className="w-full max-w-sm bg-white dark:bg-[#050505] p-6 rounded-2xl border border-neutral-200 dark:border-neutral-800 shadow-sm overflow-hidden">
      <div className="flex flex-col">
        {data.map((node, i) => (
          <LineTreeNode key={node.id} node={node} level={0} isLast={i === data.length - 1} />
        ))}
      </div>
    </div>
  );
};

export default LineTreeView;

Settings Structure Tree View

import React, { useState } from "react";
import { ChevronRight, Folder, File } from "lucide-react";
// tree data omitted for brevity

const SettingsTreeNode = ({ node, level }) => {
  const [isOpen, setIsOpen] = useState(level === 0);
  const isFolder = node.type === "folder";

  return (
    <div className="w-full border-b border-neutral-100 dark:border-neutral-800/50 last:border-0">
      <div 
        className="flex items-center py-3 pr-4 group cursor-pointer transition-colors hover:bg-neutral-50/50 dark:hover:bg-neutral-900/30"
        style={{ paddingLeft: `${level * 24 + 16}px` }}
        onClick={() => isFolder && setIsOpen(!isOpen)}
      >
        <span className="mr-3 p-1.5 rounded-lg bg-neutral-100 dark:bg-neutral-900 text-neutral-500 dark:text-neutral-400 group-hover:text-neutral-900 dark:group-hover:text-white transition-colors">
          {node.icon ? node.icon : (isFolder ? <Folder className="w-4 h-4" /> : <File className="w-4 h-4" />)}
        </span>
        <span className="text-[14px] font-medium text-neutral-700 dark:text-neutral-200 truncate flex-1">
          {node.name}
        </span>
        {isFolder && (
          <ChevronRight 
            className={`w-4 h-4 text-neutral-400 transition-transform duration-300 ${isOpen ? "rotate-90" : ""}`} 
          />
        )}
      </div>
      
      {isFolder && (
        <div 
          className="overflow-hidden transition-all duration-300 ease-in-out bg-neutral-50/30 dark:bg-neutral-900/10"
          style={{ maxHeight: isOpen ? "800px" : "0", opacity: isOpen ? 1 : 0 }}
        >
          {node.children?.map(child => (
            <SettingsTreeNode key={child.id} node={child} level={level + 1} />
          ))}
        </div>
      )}
    </div>
  );
};

const SettingsTreeView = ({ data = defaultTreeData }) => {
  return (
    <div className="w-full max-w-sm bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-2xl shadow-xl shadow-neutral-200/20 dark:shadow-none overflow-hidden">
      <div className="flex flex-col">
        {data.map(node => (
          <SettingsTreeNode key={node.id} node={node} level={0} />
        ))}
      </div>
    </div>
  );
};

export default SettingsTreeView;