Building Organizational Structure Visualization (Org Chart) for Websites
Org Chart is a hierarchical tree of organizational structure with employee cards, subordination relationships, and navigation capability through deep hierarchy levels.
Libraries
- d3-hierarchy — low-level, full control
- react-organizational-chart — simple open-source
- OrgChart.js — feature-rich, drag-drop
- @ant-design/graphs — Org Chart + other graphs
d3-hierarchy + React
import { hierarchy, tree } from 'd3-hierarchy';
import { useState, useMemo } from 'react';
interface Employee {
id: string;
name: string;
position: string;
department: string;
avatarUrl?: string;
children?: Employee[];
}
function OrgChart({ data }: { data: Employee }) {
const [expanded, setExpanded] = useState<Set<string>>(new Set([data.id]));
const [hoveredId, setHoveredId] = useState<string | null>(null);
const width = 900;
const nodeWidth = 180;
const nodeHeight = 80;
const { nodes, links } = useMemo(() => {
const root = hierarchy(data);
const treeLayout = tree<Employee>()
.nodeSize([nodeWidth + 20, nodeHeight + 40])
.separation((a, b) => (a.parent === b.parent ? 1.2 : 2));
const laid = treeLayout(root);
return {
nodes: laid.descendants(),
links: laid.links()
};
}, [data]);
const xs = nodes.map(n => n.x);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const treeWidth = maxX - minX + nodeWidth;
const treeHeight = Math.max(...nodes.map(n => n.depth)) * (nodeHeight + 40) + nodeHeight + 20;
const offsetX = (width - treeWidth) / 2 - minX;
return (
<div className="overflow-auto">
<svg width={width} height={treeHeight + 20}>
<g transform={`translate(0, 20)`}>
{links.map((link, i) => (
<path
key={i}
d={`M${link.source.x + offsetX},${link.source.y + nodeHeight}
C${link.source.x + offsetX},${(link.source.y + link.target.y + nodeHeight) / 2}
${link.target.x + offsetX},${(link.source.y + link.target.y + nodeHeight) / 2}
${link.target.x + offsetX},${link.target.y}`}
fill="none"
stroke="#d1d5db"
strokeWidth={1.5}
/>
))}
{nodes.map(node => (
<foreignObject
key={node.data.id}
x={node.x + offsetX - nodeWidth / 2}
y={node.y}
width={nodeWidth}
height={nodeHeight}
>
<EmployeeCard
employee={node.data}
isHovered={hoveredId === node.data.id}
hasChildren={!!node.data.children?.length}
isExpanded={expanded.has(node.data.id)}
onHover={setHoveredId}
onToggle={(id) => setExpanded(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
})}
/>
</foreignObject>
))}
</g>
</svg>
</div>
);
}
function EmployeeCard({ employee, isHovered, onHover, onToggle, hasChildren, isExpanded }) {
return (
<div
className={`border rounded-lg bg-white p-2 shadow-sm text-center cursor-pointer
transition-shadow ${isHovered ? 'shadow-md ring-2 ring-blue-300' : ''}`}
onMouseEnter={() => onHover(employee.id)}
onMouseLeave={() => onHover(null)}
onClick={() => onToggle(employee.id)}
>
{employee.avatarUrl && (
<img src={employee.avatarUrl} className="w-8 h-8 rounded-full mx-auto mb-1" />
)}
<p className="text-xs font-semibold text-gray-800 leading-tight">{employee.name}</p>
<p className="text-xs text-gray-500">{employee.position}</p>
{hasChildren && (
<span className="text-xs text-blue-400">{isExpanded ? '▲' : '▼'}</span>
)}
</div>
);
}
react-organizational-chart (Simplified Approach)
import { Tree, TreeNode } from 'react-organizational-chart';
function SimpleOrgChart({ data }) {
return (
<Tree
label={<OrgNode employee={data} />}
lineWidth="2px"
lineColor="#d1d5db"
lineStyle="solid"
>
{data.children?.map(child => (
<OrgSubTree key={child.id} node={child} />
))}
</Tree>
);
}
function OrgSubTree({ node }) {
return (
<TreeNode label={<OrgNode employee={node} />}>
{node.children?.map(child => (
<OrgSubTree key={child.id} node={child} />
))}
</TreeNode>
);
}
function OrgNode({ employee }) {
return (
<div className="inline-flex flex-col items-center bg-white border border-gray-200 rounded-lg px-3 py-2 shadow-sm min-w-32">
<span className="text-sm font-semibold">{employee.name}</span>
<span className="text-xs text-gray-500">{employee.position}</span>
</div>
);
}
Search in Tree
function searchInHierarchy(root: Employee, query: string): Set<string> {
const matchedIds = new Set<string>();
function traverse(node: Employee) {
const matches = node.name.toLowerCase().includes(query.toLowerCase()) ||
node.position.toLowerCase().includes(query.toLowerCase());
if (matches) matchedIds.add(node.id);
node.children?.forEach(traverse);
}
traverse(root);
return matchedIds;
}
Timeline
Org Chart with d3-hierarchy and clickable cards — 1 week. With search, zoom/pan and lazy loading of sub-nodes — 1.5–2 weeks.







