Org Chart

Arrange teams in a shared space and connect the people behind them.

Give every team a place. Arrange people in a two-dimensional workspace and trace their relationships with gently curved, named connections. Surfaces, colors, and rounded cards follow your Materialize theme.

Connect on the canvas: drag the round + on a person or team to another card or team header. A dashed line previews the connection and valid targets are highlighted. Release, enter a relationship name, and save. Escape or dropping on empty space cancels. Self-links and duplicate connections are ignored. The form remains available for keyboard use.

New connections are named “New link” automatically. Click a link’s text to rename it directly on the canvas, or hover over its line and click × to remove it. Both examples enable editable: true.

Drag a team header’s handle to move the group. Drag a person’s handle to reorder them or move them to another team. With a header focused, use arrow keys to move by 10px, or Shift + arrow keys for 40px. Scroll to explore the canvas.

3 teams · 6 people · 3 relationships. Changes stay in this page; export JSON to save your layout.

Vertical org chart

A top-to-bottom hierarchy: leadership above the design and engineering teams. Set orientation: "vertical" to attach connections to the top and bottom edges, then position the teams with x and y.

From leadership to teams

Drag the handles to move teams or people, or use the same keyboard controls as above. On smaller screens, scroll inside the chart to explore both branches.

3 teams · 6 people · 2 relationships. This example has its own layout.

const verticalHost = document.querySelector<HTMLElement>('#vertical-organization')!;
OrgChart.init(verticalHost, {
  orientation: 'vertical',
  data: {
    teams: [
      { id: 'leadership', name: 'Leadership', x: 208, y: 32 },
      { id: 'design', name: 'Design', x: 32, y: 360 },
      { id: 'engineering', name: 'Engineering', x: 384, y: 360 }
    ],
    people: [
      { id: 'alex', teamId: 'leadership', name: 'Alex Morgan' },
      { id: 'maya', teamId: 'design', name: 'Maya Patel' },
      { id: 'lea', teamId: 'engineering', name: 'Léa Martin' }
    ],
    links: [
      { from: 'leadership', fromType: 'team', to: 'design', toType: 'team', label: 'Creative direction' },
      { from: 'leadership', fromType: 'team', to: 'engineering', toType: 'team', label: 'Delivery' }
    ]
  }
});

Usage

Import OrgChart from kmaterialize and load Materialize CSS. Drag handles, connections, gestures, and printing helpers are included in the library.

<div id="organization" tabindex="0" role="region" aria-label="Organization chart"></div>
import { OrgChart } from 'kmaterialize';

const host = document.querySelector<HTMLElement>('#organization')!;
const chart = OrgChart.init(host, {
  editable: true,
  data: {
    teams: [
      { id: 'design', name: 'Design', x: 32, y: 32 },
      { id: 'engineering', name: 'Engineering', x: 480, y: 160 }
    ],
    people: [
      { id: 'maya', teamId: 'design', name: 'Maya Patel', role: 'Designer' },
      { id: 'lea', teamId: 'engineering', name: 'Léa Martin', role: 'Developer' }
    ],
    links: [
      { from: 'maya', to: 'lea', label: 'Collaborates with' }
    ]
  },
  onChange: data => console.log(data)
});

Data & options

OptionDescription
data.teamsTeams with unique id, name, and optional non-negative x/y canvas coordinates in pixels. Missing coordinates are placed automatically with space between rendered teams; supplied coordinates stay unchanged.
data.peoplePeople with unique id, existing teamId, name, and optional role.
data.linksRelationships between different endpoints: from and to IDs, with an optional label. Set fromType or toType to "team" to connect a group; omitted types default to "person". Supports person → person, group → person, person → group, and group → group.
zoom, minZoom, maxZoomInitial scale and bounds. Use chart.setZoom(value), chart.getZoom(), and chart.resetZoom() at runtime.
editableDefaults to false. Set true to rename relationships by clicking their labels on the canvas (or focus a label and press Enter). Enter or clicking away saves; Escape cancels. Clear the field to leave the link unlabeled. Hover or focus the link to reveal “Add label” when you want to name it again. The × on the line appears on hover (always visible on touch screens) and removes that relationship immediately; it also supports Enter or Space.
connectableDefaults to true. Enables drag-to-connect ports on people and teams. Set false to hide them. The cancelable orgconnect event on the host contains the proposed link in event.detail; prevent its default to handle insertion yourself. Otherwise the component inserts a link named “New link” immediately and calls onChange.
draggableShared default for person and team movement; defaults to true. The specific options below take precedence.
draggableTeamsEnable moving team boxes with pointer or keyboard. Falls back to draggable.
draggablePeopleEnable reordering people and moving them between teams. Falls back to draggable.
onChange(data)Called after moving a team or replacing data. Receives a copy of the full chart, ready to save.

Connections attach to people or group headers and follow them as they move. Layout is manual: give panels enough room for their members and connection labels. User-provided names are rendered as text. The public zoom API is chart.setZoom(1.25), chart.getZoom(), and chart.resetZoom().

Data interfaces

getData() returns an independent OrgChartData object. Pass the same structure to setData(data) to replace the entire chart. All three arrays are required, but may be empty. Fields marked with ? are optional.

import type {
  OrgChartData, OrgChartTeam, OrgChartPerson,
  OrgChartLink, OrgChartAppearance
} from 'kmaterialize';
export interface OrgChartData {
  teams: OrgChartTeam[];
  people: OrgChartPerson[];
  links: OrgChartLink[];
}

export interface OrgChartAppearance {
  color?: string;       // Item color: CSS color or theme variable.
  textColor?: string;   // Text color.
  borderColor?: string;
  accent?: string;      // Color of the accent edge.
  accentPosition?: 'top' | 'right' | 'bottom' | 'left'; // Default: bottom.
}

export interface OrgChartTeam extends OrgChartAppearance {
  id: string;          // Non-empty and unique among teams.
  name: string;        // Non-blank display name.
  x?: number;          // Optional: auto-placed when omitted.
  y?: number;          // Optional; positions are independent of zoom.
}

export interface OrgChartPerson extends OrgChartAppearance {
  id: string;          // Non-empty and unique among people.
  teamId: string;      // Must reference an existing team ID.
  name: string;        // Non-blank display name.
  role?: string;
  avatarText?: string;  // Overrides generated initials.
  avatarImage?: string; // Image URL; falls back to text if it fails to load.
  avatarFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; // Default: cover.
}

export interface OrgChartLink {
  from: string;        // Existing source person or team ID.
  to: string;          // Existing target person or team ID.
  fromType?: 'person' | 'team'; // Default: person.
  toType?: 'person' | 'team';   // Default: person.
  label?: string;      // Omit or use '' for an unlabeled link.
}

The declarations above describe the exported types; import them in your app instead of redeclaring them. A link's endpoint type determines which collection its ID refers to. Source and target must be different endpoints. People appear in their team's list in the order supplied by people.

Read, update, and save data

import { OrgChart } from 'kmaterialize';
import type { OrgChartData } from 'kmaterialize';

const host = document.querySelector<HTMLElement>('#org-chart-demo')!;
const chart = OrgChart.getInstance(host);
if (!chart) throw new Error('Initialize the chart first.');

const data: OrgChartData = chart.getData();
// Changes to this copy do not affect the canvas until setData() is called.
if (data.teams.length) data.teams[0].x = 120;
if (data.links.length) data.links[0].label = ''; // Keep the line, clear its name.
chart.setData(data);

const json = JSON.stringify(chart.getData()); // Save to your storage or API.
// Restore trusted, correctly shaped data:
const restored: OrgChartData = JSON.parse(json);
chart.setData(restored);

Independent movement controls

OrgChart.init(host, {
  data,
  draggableTeams: true,
  draggablePeople: false
});

Swap these values to allow moving people while keeping team boxes fixed. Each option controls both its drag handles and keyboard movement.

Read-only chart

These options are independent: editable controls link-label editing and deletion, connectable controls new connections, and draggable controls team and person movement. Disable all three to hide editing controls while keeping navigation and zoom available.

OrgChart.init(host, {
  data,
  editable: false,
  connectable: false,
  draggable: false
});

Custom avatars

Set avatarText to replace initials, or avatarImage to display an image inside the circular avatar. Images use object-fit: cover by default. Use contain for a logo that should remain fully visible. Failed images fall back to custom text or initials.

{ id: 'alex', teamId: 'design', name: 'Alex Morgan', avatarText: 'UX' },
{ id: 'sam', teamId: 'design', name: 'Sam Rivera',
  avatarImage: '/images/avatar.jpg', avatarFit: 'cover' }

setData() validates IDs, names, coordinates, and endpoint references, redraws the chart, and calls onChange. It replaces all three arrays; it does not merge a partial update. When removing a team, remove or reassign its people and remove links targeting that team. When removing a person, remove links targeting that person.

OrgChartData contains the organization and its positions. Options such as editable, connectable, orientation, and zoom are configured separately and are not included in getData(). TypeScript types do not validate JSON from an external service; check its structure before passing it to the component.

Colors, borders & group connections

Teams and people accept the same appearance options. color overrides the item background, textColor overrides its text, borderColor adds a full border, and accent adds a colored line directly on one edge. Choose accentPosition: top, right, bottom (default), or left. Values accept CSS colors, including Materialize theme variables. You can combine these options in the Add team and Add person forms.

const data = chart.getData();
data.teams[0].color = 'var(--md-sys-color-primary)';
data.teams[0].textColor = 'white';
data.teams[0].borderColor = 'var(--md-sys-color-primary)';
data.people[0].accent = 'var(--md-sys-color-tertiary)';
data.people[0].accentPosition = 'left';
// Group → person. IDs refer to existing items in your data.
data.links.push({
  from: data.teams[0].id, fromType: 'team',
  to: data.people[1].id, toType: 'person', label: 'Supports'
});
// Group → group.
data.links.push({
  from: data.teams[0].id, fromType: 'team',
  to: data.teams[1].id, toType: 'team', label: 'Partners with'
});
chart.setData(data);

Move handles appear only when draggable is enabled. Person handles support arrow keys: up/down to reorder, left/right to move between groups. Group handles move the whole team. Touch gestures outside a handle scroll the workspace.

Methods

Export PDF

Pinch with two fingers on the chart to zoom, or use a trackpad pinch / Ctrl + mouse wheel. Drag empty space with one finger to pan. The toolbar percentage follows gestures; 100% restores the actual card size.

Click Export PDF above, then choose Save as PDF in the browser print dialog. The complete chart (including off-screen cards and SVG connections) fits on one page of the selected format and orientation (A3 landscape by default), independently of the current zoom. Disable browser headers and footers; enable background graphics if your browser requires it. Very large charts will have smaller text.

// Call from a user click so the browser allows the preview window.
pdfButton.addEventListener('click', async () => {
  try {
    await chart.exportPdf('People & teams', { format: 'A4', orientation: 'landscape' }); // Light theme by default.
    // To export in dark mode instead:
    // await chart.exportPdf('People & teams', { theme: 'dark' });
  } catch (error) {
    console.error(error); // For example, the browser blocked the popup.
  }
});

Choose format from A0–A6, letter, legal, or tabloid. The optional orientation is landscape (default) or portrait. The chart is centered horizontally and vertically within 10mm margins, excluding unused canvas space. Browser print-dialog settings can override the requested paper size.

The PDF defaults to a white page with light-theme cards, even when the app is dark. Use the PDF: Light / Dark toolbar button to switch the export theme, or pass { theme: 'light' } / { theme: 'dark' } as the second argument. Explicit item colors are preserved. This opens the browser's print workflow, not an automatic PDF download. Keep chart-print.ts alongside the component when copying this example into your app. The original theme, layout, scroll position, and zoom are preserved.

const data = chart.getData(); // Independent copy, including positions.
chart.setData(data);           // Validate and replace teams, people, and links.
OrgChart.getInstance(host);
chart.destroy();               // Remove listeners and restore original content.

Add, edit, or remove items through getData() and setData(). Remove a person's relationships when removing that person; remove or reassign members when removing a team. Duplicate-ID errors list every conflicting team name and array index. Missing coordinates are automatically placed; explicitly invalid coordinates still throw an error before the current chart is replaced.