Tree View

Component

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

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 Apple-tier 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;