@memoir/tree
Memoir Labs

Org Chart

Render a people-based organization chart from flat reporting relationships.

OrgChart renders a rooted reporting hierarchy from app-owned people and flat manager-to-report facts.

It uses the same tree core as FamilyTree: measured cards, SVG edges, pan/scroll viewport state, accessible card props, custom React cards, and the tiny stylesheet.

Simple Mode

import { OrgChart, org } from "@memoir/tree";
import "@memoir/tree/styles.css";

const people = {
  ceo: { id: "ceo", name: "Casey", title: "CEO" },
  engineering: { id: "engineering", name: "Morgan", title: "VP Engineering" },
  design: { id: "design", name: "Riley", title: "Design Lead" },
};

const relationships = [
  org.manager("ceo", ["engineering", "design"]),
];

export function TeamChart() {
  return <OrgChart people={people} root="ceo" relationships={relationships} />;
}

You can omit card to use DefaultOrgCard.

Graph Mode

Graph mode gives each manager-report edge an app-owned identity and order. Use it when reporting facts come from persisted records rather than a hand-authored helper list.

import { OrgChart, type OrgChartGraph } from "@memoir/tree";

const graph: OrgChartGraph<Person> = {
  people,
  root: "ceo",
  reportingLinks: [
    { id: "ceo-engineering", managerId: "ceo", reportId: "engineering", relation: "manager", status: "current", order: 1 },
    { id: "ceo-design", managerId: "ceo", reportId: "design", relation: "direct", status: "former", order: 2 },
  ],
};

export function TeamChart() {
  return <OrgChart graph={graph} />;
}

Custom Card

import type { OrgCardProps } from "@memoir/tree";

type Person = (typeof people)[string];

function TeamCard({ depth, person, ...rootProps }: OrgCardProps<Person>) {
  return (
    <article {...rootProps}>
      <strong>{person.name}</strong>
      <small>{person.title ?? `level ${depth}`}</small>
    </article>
  );
}

<OrgChart
  people={people}
  root="ceo"
  relationships={relationships}
  card={TeamCard}
/>

The card receives accessibility props, keyboard handlers, stable data-* attributes, depth metadata, selection state, and the original person record. Spread the remaining props onto the card root.

renderCard is also available when you prefer a render function. Spread rootProps onto the root element:

<OrgChart
  people={people}
  root="ceo"
  relationships={relationships}
  renderCard={({ person, rootProps }) => (
    <article {...rootProps}>
      <strong>{person.name}</strong>
      <small>{person.title}</small>
    </article>
  )}
/>

Relationship Helper

org.manager("ceo", ["vp-eng", "vp-sales"], {
  relation: "manager",
  status: "current",
  order: 0,
});

org.report("vp-eng", "staff-engineer");
org.reports("ceo", ["vp-eng", "vp-sales"]);

OrgChart models people and reporting lines. Titles, teams, permissions, and actions belong on your person records or custom card props.

Common Options

<OrgChart
  people={people}
  root="ceo"
  relationships={relationships}
  selected={selectedPersonId}
  collapsed={["engineering"]}
  maxDepth={3}
  onPersonClick={(_person, personId) => setSelectedPersonId(personId)}
/>

Use selected for app-owned selection, collapsed to hide deeper reports under a person, and maxDepth to cap the rendered hierarchy.

Default spacing is compact:

{ row: 80, column: 24, padding: 24 }

Override spacing when your custom org cards need more or less room. The default interactionMode is "pan"; users can drag the canvas and non-interactive card surfaces with mouse, touch, or pen. Use "pan-page-scroll" when mouse or horizontal touch should drag the tree and vertical touch should scroll the page. Native controls inside custom cards keep their own pointer behavior.

The root is centered by default after cards are measured. Use defaultViewport for a custom uncontrolled starting position or initialViewport for explicit modes like "canvas". treeApiRef exposes centerPerson(personId), fitToSubject(), and resetViewport().

On this page