Diagrams
Dependency-free, token-themed relationship diagrams — flow, network, org chart, indented tree, mind map and fishbone.
Preview
A dependency-free static SVG org chart. Pass a nested `:tree` node — `name`, `role`, and a `children[]` array of the same shape — and it lays the hierarchy out top-down with orthogonal reporting-line connectors.
<div class="space-y-4">
<x-ui.org-chart :height="360" label="{{ __('Organization chart') }}" />
<p class="text-sm text-muted-foreground">
{{ __('A dependency-free static SVG org chart. Pass a nested `:tree` node — `name`, `role`, and a `children[]` array of the same shape — and it lays the hierarchy out top-down with orthogonal reporting-line connectors.') }}
</p>
</div>
Installation
php artisan ui:add org-chart
Registry contract
php artisan ui:add org-chart
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/org-chart.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Organization Chart (`org-chart`)
Dependency-free static SVG organization chart that lays out a nested name/role tree top-down with orthogonal reporting-line connectors.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add org-chart
```
## Usage
```blade
<div class="space-y-4">
<x-ui.org-chart :height="360" label="{{ __('Organization chart') }}" />
<p class="text-sm text-muted-foreground">
{{ __('A dependency-free static SVG org chart. Pass a nested `:tree` node — `name`, `role`, and a `children[]` array of the same shape — and it lays the hierarchy out top-down with orthogonal reporting-line connectors.') }}
</p>
</div>
```
## Props
- `tree` (mixed|null, default `null`) — A nested node tree: an object with name, role, optional department, optional avatar image URL, and a children list of the same shape. Defaults to a demo executive tree.
- `height` (int, default `360`) — Minimum SVG height in pixels; the frame grows to fit however many levels it needs.
- `label` (string, default `Organization chart`) — Accessible name for the figure.
- `showRole` (bool, default `true`) — Shows each person's role under their name.
- `showDepartment` (bool, default `false`) — Shows each person's department on their card.
- `showAvatar` (bool, default `false`) — Shows a circle with the person's initials, or their avatar image, at the start of the card.
- `showReports` (bool, default `false`) — Shows a small badge with the number of direct reports under managers.
## Use when
- Use when hierarchy, causality, network structure, or process flow is the information itself.
- Showing a reporting structure of people or teams, top-down, one manager to many direct reports.
- Needing name/role cards connected by orthogonal reporting lines, optionally with avatars, department and report counts.
## Avoid when
- Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
- The hierarchy is files or settings, not people; use indented-tree instead.
- The relationships are a general graph or a free-form brainstorm rather than a strict tree; use network-graph or mind-map instead.
## Anti-patterns
- Using a specialized diagram when linear content is clearer
## Rules
- Use the `<brok:org-chart>` tag (or `<x-ui.org-chart>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/org-chart
- Registry JSON (files, props, contract): https://brokui.dev/r/open/org-chart.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
<x-ui.org-chart :height="400" show-avatar show-department show-reports label="{{ __('Organization chart with avatars, departments and report counts') }}" />
{{-- The default reporting tree framed by chart-card with a headcount badge. --}}
<x-ui.chart-card heading="Team structure" subheading="Reporting lines" trend="8 people" trend-variant="neutral">
<x-ui.org-chart :height="360" label="{{ __('Team structure') }}" />
</x-ui.chart-card>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| tree | mixed | null | null | A nested node tree: an object with name, role, optional department, optional avatar image URL, and a children list of the same shape. Defaults to a demo executive tree. |
| height | int | 360 | Minimum SVG height in pixels; the frame grows to fit however many levels it needs. |
| label | string | Organization chart | Accessible name for the figure. |
| showRole | bool | true | Shows each person's role under their name. |
| showDepartment | bool | false | Shows each person's department on their card. |
| showAvatar | bool | false | Shows a circle with the person's initials, or their avatar image, at the start of the card. |
| showReports | bool | false | Shows a small badge with the number of direct reports under managers. |
Slots
default— Unused; the chart renders from the tree prop only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders without arguments using built-in demo data, so it previews immediately.
- Lays the tree out top-down with each level centred under its parent, connected by orthogonal reporting-line connectors.
- Dependency-free SVG: no chart library, computed and laid out entirely in Blade.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Explain hierarchy, causality, network structure, or flow.
Use when
- Use when hierarchy, causality, network structure, or process flow is the information itself.
- Showing a reporting structure of people or teams, top-down, one manager to many direct reports.
- Needing name/role cards connected by orthogonal reporting lines, optionally with avatars, department and report counts.
Avoid when
- Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
- The hierarchy is files or settings, not people; use indented-tree instead.
- The relationships are a general graph or a free-form brainstorm rather than a strict tree; use network-graph or mind-map instead.
Use instead
- List, table, or prose explanation
Anti-patterns
- Using a specialized diagram when linear content is clearer
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- The figure carries role="img" with an aria-label from label, so a screen reader hears a named summary instead of parsing raw SVG.
- Every name, role and department renders as visible SVG text, never as a hover-only tooltip.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
// A nested node tree: ['name', 'role', optional 'department', 'avatar' (image
// URL) and 'children' => [...same shape]].
'tree' => null,
'height' => 360,
'label' => 'Organization chart',
// What each card shows besides the name.
'showRole' => true,
'showDepartment' => false,
// A circle with the person's initials (or their `avatar` image) at the start of the card.
'showAvatar' => false,
// A small badge with the number of direct reports under managers.
'showReports' => false,
])
@php
// ── Demo data so it renders without args ─────────────────────────────────
$tree ??= [
'name' => __('Avery Chen'),
'role' => __('CEO'),
'department' => __('Executive'),
'children' => [
[
'name' => __('Jordan Blake'),
'department' => __('Engineering'),
'role' => __('VP Engineering'),
'children' => [
['name' => __('Sam Rivera'), 'department' => __('Engineering'), 'role' => __('Backend Lead')],
['name' => __('Priya Nair'), 'department' => __('Engineering'), 'role' => __('Frontend Lead')],
],
],
[
'name' => __('Morgan Diaz'),
'department' => __('Sales'),
'role' => __('VP Sales'),
'children' => [
['name' => __('Lee Park'), 'department' => __('Sales'), 'role' => __('Account Executive')],
['name' => __('Robin Cole'), 'department' => __('Sales'), 'role' => __('Sales Engineer')],
],
],
[
'name' => __('Taylor Quinn'),
'department' => __('Operations'),
'role' => __('VP Operations'),
'children' => [
['name' => __('Casey Wong'), 'department' => __('Operations'), 'role' => __('People Ops')],
['name' => __('Drew Patel'), 'department' => __('Operations'), 'role' => __('Finance Lead')],
],
],
],
];
$showRole = filter_var($showRole, FILTER_VALIDATE_BOOLEAN);
$showDepartment = filter_var($showDepartment, FILTER_VALIDATE_BOOLEAN);
$showAvatar = filter_var($showAvatar, FILTER_VALIDATE_BOOLEAN);
$showReports = filter_var($showReports, FILTER_VALIDATE_BOOLEAN);
// ── Geometry ─────────────────────────────────────────────────────────────
$lines = 1 + ($showRole ? 1 : 0) + ($showDepartment ? 1 : 0);
$boxW = $showAvatar ? 188 : 168; // node box width
$boxH = max($showAvatar ? 64 : 0, 24 + $lines * 17); // node box height grows with the lines it carries
$slotW = $boxW + 28; // horizontal slot per leaf
$avatarR = 18;
$textX = $showAvatar ? 16 + 2 * $avatarR + 12 : 0; // text start (from the box's left edge) when an avatar leads
$initials = function (string $name): string {
$parts = preg_split('/\s+/', trim($name)) ?: [];
$first = mb_substr($parts[0] ?? '', 0, 1);
$last = count($parts) > 1 ? mb_substr(end($parts), 0, 1) : '';
return mb_strtoupper($first.$last);
};
$levelGap = max(96, (int) round((int) $height / 4)); // vertical distance between levels
$pad = 16; // viewBox padding
// ── Tidy top-down layout ──────────────────────────────────────────────────
// Walk the tree depth-first. Each LEAF takes the next x slot; a parent's x is
// the midpoint of its children. y is depth * levelGap. Collect flat nodes +
// parent→child edges for orthogonal (elbow) connectors.
$nodes = [];
$edges = [];
$leafCursor = 0;
$layout = function (array $node, int $depth, ?int $parentIndex) use (
&$layout, &$nodes, &$edges, &$leafCursor, $slotW, $levelGap
) {
$children = $node['children'] ?? [];
$index = count($nodes);
// Reserve this node's slot in the flat list before recursing so children
// can reference their parent by index.
$nodes[$index] = [
'name' => $node['name'] ?? '',
'role' => $node['role'] ?? '',
'department' => $node['department'] ?? '',
'avatar' => $node['avatar'] ?? null,
'reports' => count($children),
'x' => 0.0,
'y' => $depth * $levelGap,
];
if ($parentIndex !== null) {
$edges[] = ['parent' => $parentIndex, 'child' => $index];
}
if (empty($children)) {
// Leaf: claim the next horizontal slot (centre of the slot).
$nodes[$index]['x'] = $leafCursor * $slotW + $slotW / 2;
$leafCursor++;
return $nodes[$index]['x'];
}
$childXs = [];
foreach ($children as $child) {
$childXs[] = $layout($child, $depth + 1, $index);
}
// Parent centred over the span of its children.
$nodes[$index]['x'] = (min($childXs) + max($childXs)) / 2;
return $nodes[$index]['x'];
};
$layout($tree, 0, null);
// ── viewBox extents (computed from the laid-out nodes) ────────────────────
$maxX = 0.0;
$maxY = 0.0;
foreach ($nodes as $n) {
$maxX = max($maxX, $n['x'] + $boxW / 2);
$maxY = max($maxY, $n['y'] + $boxH);
}
$vbW = $maxX + $boxW / 2 + $pad; // half a box of breathing room each side
$vbH = $maxY + $pad;
// Half-step where the horizontal bus sits between two levels.
$busDrop = (int) round($levelGap / 2);
@endphp
<div
data-slot="org-chart"
role="img"
aria-label="{{ $label }}"
{{-- Wide charts scroll horizontally; keep the scroll region keyboard-focusable. --}}
tabindex="0"
{{ $attributes->merge(['class' => 'w-full overflow-x-auto text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring']) }}
>
<svg
viewBox="0 0 {{ round($vbW, 2) }} {{ round($vbH, 2) }}"
width="100%"
height="{{ (int) $height }}"
preserveAspectRatio="xMidYMin meet"
fill="none"
aria-hidden="true"
class="h-auto w-full min-w-(--org-chart-min-w)"
style="--org-chart-min-w: {{ (int) round($vbW) }}px"
>
{{-- Connectors: parent bottom-centre → horizontal bus → child top-centre. --}}
@foreach ($edges as $edge)
@php($p = $nodes[$edge['parent']])
@php($c = $nodes[$edge['child']])
@php($pBottomY = $p['y'] + $boxH)
@php($cTopY = $c['y'])
@php($busY = $cTopY - $busDrop)
<path
data-slot="org-chart-edge"
class="stroke-border"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
d="M{{ round($p['x'], 2) }} {{ round($pBottomY, 2) }} V{{ round($busY, 2) }} H{{ round($c['x'], 2) }} V{{ round($cTopY, 2) }}"
/>
@endforeach
{{-- Nodes: rounded card box + avatar / name / role / department / reports. --}}
@foreach ($nodes as $i => $n)
@php($nx = $n['x'] - $boxW / 2)
@php($ny = $n['y'])
@php($tx = $showAvatar ? $nx + $textX : $n['x'])
@php($anchor = $showAvatar ? 'start' : 'middle')
@php($firstY = $ny + ($boxH - ($lines - 1) * 17) / 2)
<g data-slot="org-chart-node">
<rect
class="fill-card stroke-border"
x="{{ round($nx, 2) }}"
y="{{ round($ny, 2) }}"
width="{{ $boxW }}"
height="{{ $boxH }}"
rx="10"
fill="currentColor"
stroke="currentColor"
stroke-width="1"
/>
@if ($showAvatar)
@php($ax = $nx + 16 + $avatarR)
@php($ay = $ny + $boxH / 2)
@if ($n['avatar'])
<clipPath id="org-avatar-{{ $i }}-{{ substr(md5($label), 0, 6) }}"><circle cx="{{ round($ax, 2) }}" cy="{{ round($ay, 2) }}" r="{{ $avatarR }}" /></clipPath>
<image
href="{{ $n['avatar'] }}"
x="{{ round($ax - $avatarR, 2) }}"
y="{{ round($ay - $avatarR, 2) }}"
width="{{ 2 * $avatarR }}"
height="{{ 2 * $avatarR }}"
preserveAspectRatio="xMidYMid slice"
clip-path="url(#org-avatar-{{ $i }}-{{ substr(md5($label), 0, 6) }})"
/>
@else
<circle class="fill-muted" cx="{{ round($ax, 2) }}" cy="{{ round($ay, 2) }}" r="{{ $avatarR }}" fill="currentColor" />
<text
class="pointer-events-none fill-foreground text-2xs font-semibold"
x="{{ round($ax, 2) }}"
y="{{ round($ay, 2) }}"
text-anchor="middle"
dominant-baseline="central"
>{{ $initials($n['name']) }}</text>
@endif
@endif
<text
class="pointer-events-none fill-foreground text-xs font-semibold"
x="{{ round($tx, 2) }}"
y="{{ round($firstY, 2) }}"
text-anchor="{{ $anchor }}"
dominant-baseline="middle"
>{{ $n['name'] }}</text>
@php($lineY = $firstY)
@if ($showRole)
@php($lineY += 17)
<text
class="pointer-events-none fill-muted-foreground text-2xs"
x="{{ round($tx, 2) }}"
y="{{ round($lineY, 2) }}"
text-anchor="{{ $anchor }}"
dominant-baseline="middle"
>{{ $n['role'] }}</text>
@endif
@if ($showDepartment)
@php($lineY += 17)
<text
class="pointer-events-none fill-muted-foreground text-2xs font-medium uppercase tracking-wide"
x="{{ round($tx, 2) }}"
y="{{ round($lineY, 2) }}"
text-anchor="{{ $anchor }}"
dominant-baseline="middle"
>{{ $n['department'] }}</text>
@endif
@if ($showReports && $n['reports'] > 0)
{{-- Direct-report count, straddling the bottom edge. --}}
<rect
class="fill-background stroke-border"
x="{{ round($n['x'] - 12, 2) }}"
y="{{ round($ny + $boxH - 9, 2) }}"
width="24"
height="18"
rx="9"
stroke-width="1"
/>
<text
class="pointer-events-none fill-muted-foreground text-2xs font-semibold tabular-nums"
x="{{ round($n['x'], 2) }}"
y="{{ round($ny + $boxH, 2) }}"
text-anchor="middle"
dominant-baseline="central"
>{{ $n['reports'] }}</text>
@endif
</g>
@endforeach
</svg>
</div>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.1 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0