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.tsxAbout
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
<OrgChart
nodes={people}
defaultDepth={2}
onSelect={(person) => open(person.id)}
/>Examples
A team, with an unmanaged contractor
<OrgChart nodes={people} onSelect={(person) => open(person.id)} />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'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| nodes | OrgNode[] | required | Flat { 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 | — | — |
| selectedId | string | — | — |
| defaultDepth | number | 2 | Depth open on first render. Infinity expands everything. |
| stackAfter | number | 4 | Past 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 | — | — |
| emptyLabel | string | 'No people.' | — |
| label | string | 'Organisation chart' | — |
Types
OrgNode
A reporting hierarchy, drawn top-down with connectors.
| Field | Type | Description |
|---|---|---|
| id | string | — |
| name | ReactNode | — |
| title? | ReactNode | — |
| avatar? | ReactNode | — |
| managerId? | string | null | — |
| meta? | ReactNode | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| vs Tree | spans, not depth | A 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. |
| orphans | rendered as roots | A vacant manager slot, a contractor, a transfer pointing at a deleted record. Dropping those rows makes the chart quietly wrong. |
| cycles | reported | A reports to B reports to A is detected by reachability and surfaced through onError rather than recursed into. |