Org Chart

A reporting hierarchy drawn top-down with connectors, built from flat { id, managerId } rows. Orphans and multiple roots are rendered rather than dropped; cycles are reported.

src/components/ui/org-chart.tsx

About

From the component's own source, where it was written.

A reporting hierarchy, drawn top-down with connectors.

**Why not Tree.** A tree is an indented list — it is the right shape for a filesystem, where depth is all that matters and breadth is unbounded. An org chart is read for *spans*: how many people report to this person, and are these two peers. Siblings side by side answer both at a glance; siblings stacked vertically answer neither, and an org of forty people becomes forty indented rows you have to count.

**It builds the tree from flat { id, managerId } rows**, because that is how every HR system, directory and database actually stores it. Requiring pre-nested input pushes the same recursion into every caller.

**Multiple roots and orphans are rendered, not dropped.** Real directories have a vacant manager slot, a contractor with no manager, a recent transfer pointing at a deleted record. Silently omitting those rows makes the chart quietly wrong; they are shown as additional roots so the gap is visible. A cycle — A reports to B reports to A — is detected and reported rather than recursed into.

Nodes are <li> inside nested <ul>s: the semantics are already a tree, so the connectors are drawn with borders on top of correct markup rather than replacing it.

Install

npx astralyx-ui add org-chart

Usage

import { OrgChart } from '@/components/ui/org-chart'

<OrgChart nodes={people} onSelect={(person) => open(person.id)} />

Composer

Change the props and the source below follows.

Organisation chart

  • CTO
    • VP Engineering
      • Platform lead
      • Product lead
    • VP Design
      • Designer
      • Researcher
  • Contractor — security
Props
tsx
<OrgChart
  nodes={people}
  defaultDepth={2}
  onSelect={(person) => open(person.id)}
/>

Examples

A team, with an unmanaged contractor

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

OrgChart

Extends Omit<ComponentProps<'div'>, 'onSelect' | 'onError'>.

PropTypeDefaultDescription
nodesOrgNode[]requiredFlat { id, name, title?, managerId } — how every HR system, directory and database actually stores it. Requiring pre-nested input pushes the same recursion into every caller.
onSelect(node: OrgNode) => void——
selectedIdstring——
defaultDepthnumber2Depth open on first render. Infinity expands everything.
stackAfternumber4Past this many children a node lays them out in a column with a spine, rather than a very long horizontal rule.
onError(error: Error) => void——
emptyLabelstring'No people.'—
labelstring'Organisation chart'—

Types

OrgNode

A reporting hierarchy, drawn top-down with connectors.

FieldTypeDescription
idstring—
nameReactNode—
title?ReactNode—
avatar?ReactNode—
managerId?string | null—
meta?ReactNode—

Behaviour

TopicDetailDescription
vs Treespans, not depthA tree is an indented list, right for a filesystem. An org chart is read for spans — how many report to this person, are these two peers — and siblings side by side answer both at a glance.
orphansrendered as rootsA vacant manager slot, a contractor, a transfer pointing at a deleted record. Dropping those rows makes the chart quietly wrong.
cyclesreportedA reports to B reports to A is detected by reachability and surfaced through onError rather than recursed into.