Org chart
10 people and 1 open role. Use the arrow keys to move, Right and Left to expand and collapse.
- Hannah OkaforChief executive officerLeadership10 reports
- Daniel WeissChief technology officerEngineering4 reports
- Ahmed RazaEngineering managerEngineering2 reports
- Chloe MartinData analyst (dotted line to the CTO)
- Mia JensenHead of productProduct1 reports
- Priya NairHead of peoplePeople Ops2 reports
Ahmed Raza
Engineering manager · Lahore
Reports to Daniel Weiss · 2 people in their team
org-chart.tsx
"use client";
import { useState } from "react";
import { LuChevronsDownUp, LuChevronsUpDown } from "react-icons/lu";
import { Button, OrgChart, OrgNode } from "@nexera-ui/react";
interface Person {
id: string;
name: string;
title: string;
department?: string;
location?: string;
vacant?: boolean;
dotted?: boolean;
reports?: Person[];
}
const TREE: Person = {
id: "hannah",
name: "Hannah Okafor",
title: "Chief executive officer",
department: "Leadership",
location: "London",
reports: [
{
id: "daniel",
name: "Daniel Weiss",
title: "Chief technology officer",
department: "Engineering",
location: "Berlin",
reports: [
{
id: "ahmed",
name: "Ahmed Raza",
title: "Engineering manager",
department: "Engineering",
location: "Lahore",
reports: [
{ id: "luis", name: "Luis Ortega", title: "Frontend engineer", location: "Madrid" },
{
id: "open-be",
name: "Open role",
title: "Senior backend engineer",
department: "Engineering",
vacant: true,
},
],
},
{
id: "chloe",
name: "Chloe Martin",
title: "Data analyst (dotted line to the CTO)",
location: "Paris",
dotted: true,
},
],
},
{
id: "mia",
name: "Mia Jensen",
title: "Head of product",
department: "Product",
location: "Copenhagen",
reports: [
{ id: "ayesha", name: "Ayesha Khan", title: "Senior product designer", location: "London" },
],
},
{
id: "priya",
name: "Priya Nair",
title: "Head of people",
department: "People Ops",
location: "London",
reports: [
{ id: "sara", name: "Sara Malik", title: "Payroll specialist", location: "Lisbon" },
{ id: "yuki", name: "Yuki Tanaka", title: "Recruiter", location: "Tokyo" },
],
},
],
};
const countAll = (person: Person): number =>
(person.reports ?? []).reduce((sum, report) => sum + 1 + countAll(report), 0);
const flatten = (person: Person): Person[] => [person, ...(person.reports ?? []).flatMap(flatten)];
const EVERYONE = flatten(TREE);
const MANAGERS = EVERYONE.filter((p) => (p.reports ?? []).length > 0).map((p) => p.id);
function renderPerson(person: Person, selected: string, onSelect: (id: string) => void) {
const children = (person.reports ?? []).map((report) => renderPerson(report, selected, onSelect));
const department = person.department === undefined ? {} : { department: person.department };
if (person.vacant === true) {
return (
<OrgNode
key={person.id}
id={person.id}
variant="vacant"
jobTitle={person.title}
isSelected={false}
{...department}
>
{children}
</OrgNode>
);
}
const reports = person.reports?.length ? { reports: `${String(countAll(person))} reports` } : {};
return (
<OrgNode
key={person.id}
id={person.id}
name={person.name}
jobTitle={person.title}
lineStyle={person.dotted === true ? "dotted" : "solid"}
isSelected={selected === person.id}
onPress={() => {
onSelect(person.id);
}}
{...department}
{...reports}
>
{children}
</OrgNode>
);
}
/** Org chart: Northwind's reporting lines. Expand a manager to see their team; select a person to see their details. */
export default function OrgChartBlock() {
const [expanded, setExpanded] = useState<Set<string | number>>(new Set(["hannah", "daniel"]));
const [selected, setSelected] = useState("ahmed");
const person = EVERYONE.find((p) => p.id === selected);
const manager = EVERYONE.find((p) => p.reports?.some((r) => r.id === selected));
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid w-full max-w-6xl gap-5">
<header className="flex flex-wrap items-end justify-between gap-3">
<div className="grid gap-1">
<h2 className="text-heading-h2 text-primary">Org chart</h2>
<p className="text-body-default text-secondary">
{EVERYONE.length - 1} people and 1 open role. Use the arrow keys to move, Right and
Left to expand and collapse.
</p>
</div>
<div className="flex flex-wrap gap-2">
<Button
variant="secondary"
leadingIcon={<LuChevronsUpDown />}
onPress={() => {
setExpanded(new Set(MANAGERS));
}}
>
Expand all
</Button>
<Button
variant="ghost"
leadingIcon={<LuChevronsDownUp />}
onPress={() => {
setExpanded(new Set(["hannah"]));
}}
>
Collapse
</Button>
</div>
</header>
<div className="rounded-xl border border-default bg-surface p-4 shadow-xs">
<OrgChart
aria-label="Northwind reporting lines"
expandedKeys={expanded}
onExpandedChange={(keys) => {
setExpanded(new Set(keys));
}}
>
{renderPerson(TREE, selected, setSelected)}
</OrgChart>
</div>
{person ? (
<section
aria-live="polite"
aria-labelledby="selected-title"
className="grid gap-1 rounded-xl border border-default bg-surface p-4 @3xl:max-w-md"
>
<h3 id="selected-title" className="text-heading-h4 text-primary">
{person.name}
</h3>
<p className="text-body-small text-secondary">
{person.title}
{person.location ? ` · ${person.location}` : ""}
</p>
<p className="text-body-small text-secondary">
{manager ? `Reports to ${manager.name}` : "Top of the organisation"} ·{" "}
{countAll(person)} people in their team
</p>
</section>
) : null}
</div>
</div>
);
}